useReducer state is apparently not reactive
I have had an idea to build a React store that is somewhat like Vuex. I want a wrapper component to receive an object of values and provide, using useContext
, an object back that attaches a state
and set
to each key in the object.
For example, if you wrapped your app in something like <StoreProvider value={{ color: 'red' }}>
you could in any child write const { color } = useContext(StoreContext)
, use its value with color.state
, and then call color.set('blue')
to globally change that value.
I have written a best-attempt at the store with useReducer
:
import React, {
createContext,
useContext,
useReducer
} from 'react'
const StoreContext = createContext(null)
export const useStoreContext = () => useContext(StoreContext)
export const StoreProvider = ({ children, value }) => {
const reducer = (state, action) => {
state[action.type].state = action.payload
return state
}
Object.keys(value).map(key => {
let state = value[key]
value[key] = {
state: state,
set: (setter) => dispatch({ type: key, payload: setter })
}
})
const [store, dispatch] = useReducer(reducer, value)
return (
<StoreContext.Provider value={store}>
{children}
</StoreContext.Provider>
)
}
And a little demo to see if it works:
import React from 'react'
import ReactDOM from 'react-dom'
import { StoreProvider, useStoreContext } from './store'
const App = () => {
const { color } = useStoreContext()
return (
<>
<h1>{color.state}</h1>
<button onClick={() => color.set('yellow')}>Change color</button>
<button onClick={() => console.log(color)}>Log</button>
</>
)
}
ReactDOM.render(
<StoreProvider value={{
color: 'orange'
}}>
<App />
</StoreProvider>,
document.getElementById('root')
)
In the above, the <h1>
renders as 'orange' correctly, but doesn't update to 'yellow' when the set('yellow')
is run. If I log color, however, the state
has updated to 'yellow' -- which I guess means the state isn't reactive.
Have I done something really stupid? I'm pretty nooby with React and have never used useReducer
before.
Solution 1:
I feel you missing a very important point in React that you should not ever mutate the state object. state[action.type].state = action.payload
is a state mutation. On top of the mutation, you simply return the same state object. In order for React state updates to work correctly you necessarily need to return new object references.
const reducer = (state, action) => {
return {
...state, // <-- shallow copy state
[action.type]: {
...state[action.type], // <-- shallow copy nested state
state: action.payload,
}
};
}
You are also misusing the array.map
callback; you should really use a forEach
is you are iterating an array and issuing side-effects. array.map
is considered a pure function.
Object.keys(value).forEach(key => {
let state = value[key]
value[key] = {
state: state,
set: (setter) => dispatch({ type: key, payload: setter })
}
});
I couldn't get your initial value
mapping to work correctly, seems it was doing some extra nesting of properties that didn't quite match how you were accessing in the UI nor updating. I suggest the following for computing your initial reducer state value. Iterate the array of key-value pairs of the value
object passed to the context, reducing into an object with the same keys and the value and setter nested correctly.
const StoreProvider = ({ children, value }) => {
const reducer = (state, action) => {
return {
...state,
[action.type]: {
...state[action.type],
state: action.payload
}
};
};
const state = Object.entries(value).reduce(
(state, [key, value]) => ({
...state,
[key]: {
state: value,
set: (setter) => dispatch({ type: key, payload: setter })
}
}),
{}
);
const [store, dispatch] = useReducer(reducer, state);
return (
<StoreContext.Provider value={store}>{children}</StoreContext.Provider>
);
};