50

We are creating a large front-end application.

We are using React-Redux for it

We are creating some reusable components.

This question is regarding having multiple instance of same reusable redux react components on the same page/route

Problem details:

We have a Sectionheader component. Which is bound to redux state.

It listens to the header property reducer SectionheaderReducer.

As we have 2 instances of this Sectionheader on the page both tend to show same values as they are bound to the same store state-property.

How to make the redux based reusable react component configurable? So that each instance can have different value of header property for reducer SectionheaderReducer

AncientSwordRage
7,27021 gold badges100 silver badges195 bronze badges
asked Mar 20, 2017 at 14:34

5 Answers 5

50

You need to implement some way of namespacing the instances. This can be as basic as passing in a key to differentiate the components and reducers.

You can use the ownProps in your mapStateToProps function to guide the mapping to a namespace

const mapStateToProps = (state, ownProps) {
 let myState = state[ownProps.namespace]
 return {
 myState.value
 }
}

The same method can be used to pass on a namespace to the mapDispatchToProps

const mapDispatchToProps = (dispatch, ownProps) {
 return {
 myAction: (myParam) => dispatch(myAction(ownProps.namespace, myParam))
 }
}

Just remember to use the namespace in the action type so the reducers don't tread on toes

const myAction => (namespace, myParam) {
 return { type: `${namespace}/${MY_TYPE_CONSTANT}`, myParam }
}

And make sure the reducer is namespaced too

const myReducer = (namespace) => (state = initialState, action) => {
 switch(action.type) {
 case `${namespace}/${MY_TYPE_CONSTANT}`:
 return { ...state, action.myParam }
 default:
 return state
 {
}

Now add the 2 namespaced reducers when combining reducers

combineReducers({
 myInstance1 : myReducer('myInstance1')
 myInstance2 : myReducer('myInstance2')
}

Finally pass the namespace to each instance

render() {
 return (
 <div>
 <MyComponent namespace='myInstance1' />
 <MyComponent namespace='myInstance2' />
 </div>
 )
}

Disclaimer: I am the main contributor on the following library.

redux-subspace can provide a more advanced namespacing implementation without you having to reimplement this pattern for every component you want to have multiple instances for.

Creating the reducers is similar to above

const reducer = combineReducers({ 
 myInstance1: namespaced('myInstance1')(myReducer)
 myInstance2: namespaced('myInstance2')(myReducer)
})

Then SubspaceProvider can be used to switch out the state for each component

render() {
 return (
 <div>
 <SubspaceProvider mapState={state => state.myInstance1} namespace='myInstance1'>
 <MyComponent />
 </SubspaceProvider>
 <SubspaceProvider mapState={state => state.myInstance2} namespace='myInstance2'>
 <MyComponent />
 </SubspaceProvider>
 </div>
 )
}

Just ensure you also change your mapStateToProps function to so start traversing from the subtree mapped in the provider

const mapStateToProps = (state) {
 return {
 state.value
 }
}

There is also a Higher-Order Component if you prefer to reduce nesting.

answered Mar 21, 2017 at 22:56
Sign up to request clarification or add additional context in comments.

10 Comments

Hi Michael - I'm facing the same problem where I have multiple instances of a component on a page using the same action & reducer. I tried your solution but couldn't quite make it work. Any chance you could take a quick look and perhaps update the code of my Question? stackoverflow.com/questions/44640679/…
This answer... is why I would never use Redux or any of these singleton patterns. It is completely against the notion of software development to need to do this kind of thing to support two instances of something. This is solved in Vue, React State, Angular (2/4+), Polymer and web components in general. Yet the React community continues to push Redux for no apparent reason.
I agree with using props in mapStateToProps is needed, but all the logic can be done within the component the OP is writing so that the OP's component only requires a unique key passed as a prop, without forcing users of the component to have to combine multiple reducers. Internally the OP's component would use mapStateToProps to include the key in it's dispatch actions, but the component's reducer can be responsible for allocating effective sub-namespaces within it's own namespace, and (factory) selectors responsible for ensuring each component uses the correct sub-namespace.
@MichaelPeyper what if you have N instances where N can and will change and determined by an API call? In this case, I'm not sure how many instances of my component I'm going to have. Any ideas how to deal with this situation? Currently, I'm using local state for that component which works fine.
For anyone coming across this now, as a response to the comment by @coding: that's correct, the redux singelton pattern isn't very reusable, but that's not the problem it's solving. Redux is really useful when you want components anywhere in your tree to modify / use the same data without passing around tons of props and callbacks (i.e. exactly when you want a singleton / global). Internal component state is a better approach when you need to render N copies of a component that all behave the same way. This answer is a happy medium for when you need, say, 3 copies of the same reducer.
|
2

I've implemented it in a different way, without actually changing the action name with a namespace.

Rather, I added infra functions which will intercept the action creators and add meta-data to each action. (following FSA) That way you don't need to change your reducer or the mapStateToProps function.

Also it is compatible with redux-thunk.

Should be easy to use... reducer-action-interceptor

answered Jan 21, 2018 at 8:48

Comments

1

If you're using Redux Toolkit, you could consider this pattern. Your createSlice initialState would be an object, an empty object to start with.

createSlice({
 name: "counters",
 initialState: {},
 reducers: {}
})

You would then have a few reducers that create and delete initial state (which would be triggered on mounting and unmounting of the component)

createSlice({
 name: "counters",
 initialState: {},
 reducers: {
 // will be use on mounting of the component
 createState(state, { payload: id }: { payload: number | string }) {
 // create state only if there's no state for the id
 if (!state[id]) {
 state[id] = 0; // this is where you actually define the initial state instead of up there 👆...
 }
 },
 deleteState(state, { payload: id }: { payload: number | string }) {
 delete state[id];
 },
 }
})

You can have a look at this file on Stackblitz for the complete code.

Next, we'll add the increment and decrement actions and a selector function named selectCounters:

import { createSlice } from '@reduxjs/toolkit';
import { IRootState } from '../store';
export const {
 reducer: countersReducer,
 actions: { 
 increment, decrement, 
 createState: createCounterState, 
 deleteState: deleteCounterState },
} = createSlice({
 name: 'counter',
 initialState: {},
 reducers: {
 createState(state, { payload: id }: { payload: number | string }) {
 // create state only if there's no state for the id
 if (!state[id]) {
 state[id] = 0; // this is where you actually define the initial state instead of up there 👆...
 }
 },
 deleteState(state, { payload: id }: { payload: number | string }) {
 delete state[id];
 },
 increment(state, { payload: id }: { payload: number | string }) {
 state[id] = state[id] + 1;
 },
 decrement(state, { payload: id }: { payload: number | string }) {
 state[id] = state[id] - 1;
 },
 },
});
export const selectCounters = (state: IRootState) => state.counters;

Next, lets work on our integrated Counter component. Note these two point:

  1. we create the component state on mounting i.e. inside the useEffect and delete it when it unmounts.
  2. The <Counter /> accepts an id prop to differentiate between counters
import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {
 createCounterState,
 decrement,
 increment,
 selectCounters,
 deleteCounterState,
} from './countReducer';
export const Counter = ({ id }: { id: number | string }) => {
 const counters = useSelector(selectCounters);
 const dispatch = useDispatch();
 useEffect(() => {
 dispatch(createCounterState(id));
 return () => {
 dispatch(deleteCounterState());
 };
 }, []);
 return (
 <div style={{ display: 'flex', gap: '8px', fontSize: '20px' }}>
 <button type="button" onClick={() => dispatch(decrement(id))}>
 -
 </button>
 {counters[id]}
 <button type="button" onClick={() => dispatch(increment(id))}>
 +
 </button>
 </div>
 );
};

That is it!! Now, we can use the re-usable <Counter id={"hello or any other number or string id"} /> as many times we like:

export const App: FC<{ name: string }> = ({ name }) => {
 return (
 <Provider store={store}>
 {/** You can pass a numeric or string id */}
 <Counter id={1} />
 <br />
 <Counter id={'foo'} />
 <br />
 <Counter id={'bar'} />
 <br />
 <Counter id={10} />
 </Provider>
 );
};

Here's the working example on Stackblitz: https://stackblitz.com/edit/stackblitz-starters-68admb?file=src%2FApp.tsx

enter image description here

answered Dec 12, 2023 at 3:40

Comments

0

I interpreted the question to mean:

  • you have content data in the store (e.g. the sections and their titles)
  • you have components for drawing bits of the data (e.g. your <SectionHeader />)
  • you want to display more than one section on a page but currently all your headers have the same text

One possible solution would have you add the idea of "sections" to your store. You'd create reducers that manage the content structure of the data. E.G. the store state, at one time, may look like this:

{ 
 sections: {
 0: {
 header: 'My section title',
 content: 'Whatever your content is'
 },
 1: {
 header: 'My other section title',
 content: 'Loads of lovely writing or hrefs to images or whatever'
 }
 }
}

```

You would then have a "container component" or "layout component" or "smart component" (they have many names), that "knows" that you want to use section 2 and section 4 on a particular page. How it knows this, is up to you. Perhaps you hard-code the indices (because it will always be the same), perhaps you have a filtering rule, perhaps you have another field in the store which defines the choices... etc.

The container component would then pass the chosen heading into the "dumb" , perhaps like this:

<SectionHeader>{sections[2].header}</SectionHeader>

or

<SectionHeader title={sections[2].header} />
answered Jun 27, 2018 at 10:35

Comments

0

Convert our component into dumb(stateless) component so that those will reused easily without any complications.

answered Mar 21, 2021 at 6:50

Comments

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.