Reputation: 503
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
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
Upvotes: 50
Views: 24507
Reputation: 23727
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:
useEffect
and delete it when it unmounts.<Counter />
accepts an id prop
to differentiate between countersimport { 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
Upvotes: 1
Reputation: 8726
Convert our component into dumb(stateless) component so that those will reused easily without any complications.
Upvotes: 0
Reputation: 6944
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.
Upvotes: 50
Reputation: 638
I interpreted the question to mean:
<SectionHeader />
)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} />
Upvotes: 0
Reputation: 5367
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
Upvotes: 2