Stateful context not updating properly
Closed this issue · 5 comments
react-hooks-testing-libraryversion: 8.0.1reactversion: 18.2.0react-domversion (if applicable): 18.2.0react-test-rendererversion (if applicable):nodeversion: v16.15.0npm(oryarn) version: yarn v1.22.19
Relevant code or config:
I'm having trouble testing a custom hook that uses a context that basically sets and gets values.
Here's a simplified toy version:
type State = { a?: number; b?: number; c?: number }
const defaultState: State = {}
const StateContext = React.createContext<{
state: State
setState: React.Dispatch<React.SetStateAction<State>>
}>({
state: defaultState,
setState: () => {},
})
const StateProvider = ({ children }: { children: React.ReactNode }) => {
const [state, setState] = React.useState<State>(defaultState)
return (
<StateContext.Provider value={{ state, setState }}>
{children}
</StateContext.Provider>
)
}
export const useStateContext = ({ context }: { context: typeof StateContext }) => {
const { state, setState } = React.useContext(context)
const sum = Object.values(state).reduce((a, b) => a + (b ?? 0), 0)
return { state, setState, sum }
}What you did:
If I implement in app it works as expected:
const Button = ({ newState }: { newState: State }) => {
const { state, setState, sum } = useStateContext({ context: StateContext })
useEffect(() => console.log({ state, sum }), [state, sum])
return (
<button
onClick={() => {
console.log({ newState })
setState(newState)
}}
>
{JSON.stringify(newState)}
</button>
)
}
export const App = () => {
return (
<StateProvider>
<Button newState={{ a: 1, b: 2, c: 3 }} />
<Button newState={{ a: 2, b: 3, c: 4 }} />
</StateProvider>
} This logs out the new sum correctly when the useStateContext is used when buttons are clicked.
When I try and test though:
it.only('useStateContext', () => {
type State = { a?: number; b?: number; c?: number }
const defaultState: State = {}
const StateContext = React.createContext<{
state: State
setState: React.Dispatch<React.SetStateAction<State>>
}>({
state: defaultState,
setState: () => {},
})
const StateProvider = ({ children }: { children: React.ReactNode }) => {
const [state, setState] = React.useState<State>(defaultState)
return (
<StateContext.Provider value={{ state, setState }}>
{children}
</StateContext.Provider>
)
}
const useStateContext = ({ context }: { context: typeof StateContext }) => {
const { state, setState } = React.useContext(context)
const sum = Object.values(state).reduce(
(acc, curr) => acc + (curr ?? 0),
0
)
return { state, setState, sum }
}
const {
result: {
current: { state, setState, sum },
},
rerender,
} = renderHook(
() => {
return useStateContext({ context: StateContext })
},
{ wrapper: StateProvider }
)
act(() => {
setState({ a: 1, b: 2, c: 3 })
rerender()
})
expect(state).toEqual({ a: 1, b: 2, c: 3 })
expect(sum).toEqual(6)
act(() => {
setState({ a: 2, b: 3, c: 4 })
rerender()
})
expect(state).toEqual({ a: 2, b: 3, c: 4 })
expect(sum).toEqual(9)
})it never updates the state
● useFilterTrack › useStateContext
expect(received).toEqual(expected) // deep equality
- Expected - 5
+ Received + 1
- Object {
- "a": 1,
- "b": 2,
- "c": 3,
- }
+ Object {}
202 | rerender()
203 | })
> 204 | expect(state).toEqual({ a: 1, b: 2, c: 3 })
| ^
205 | expect(sum).toEqual(6)
206 |
207 | act(() => {
What happened:
Reproduction:
Codesandbox of code: https://codesandbox.io/s/frosty-maria-ccydpt?file=/src/App.tsx
The test is self contained. You should be able to copy-paste the it.only block and run.
Problem description:
I have a working stateful context that I can verify just by using the app, but cannot seem to test it properly
Suggested solution:
This works:
const Counter = () => {
const { sum } = useStateContext({ context: StateContext });
return <div>{sum}</div>;
};
export const App = () => {
return (
<StateProvider>
<Button newState={{ a: 1, b: 2, c: 3 }} />
<Button newState={{ a: 2, b: 3, c: 4 }} />
<Counter />
</StateProvider>
);
};But this does not, because the usage of the context is outside of the provider:
export const App = () => {
const { sum } = useStateContext({ context: StateContext });
return (
<StateProvider>
<Button newState={{ a: 1, b: 2, c: 3 }} />
<Button newState={{ a: 2, b: 3, c: 4 }} />
<div>{sum}</div>
</StateProvider>
);
};It appears the test is behaving like the latter, so perhaps wrapper isn't wrapping correctly?
Hi @sliu-cais,
Sorry, I missed the notification for this issue. Did you get it sorted in the end?
one thing that immediately jumped out at me was that you are destructing result which breaks hook updates (see the note in this section of the docs).
Hi @sliu-cais,
Sorry, I missed the notification for this issue. Did you get it sorted in the end?
one thing that immediately jumped out at me was that you are destructing
resultwhich breaks hook updates (see the note in this section of the docs).
I eventually went with a completely different approach so this wasn't needed in the end. But you're saying if I were to do this it would likely work, correct?
const {
result,
rerender,
} = renderHook(
() => {
return useStateContext({ context: StateContext })
},
{ wrapper: StateProvider }
)
act(() => {
result.current.setState({ a: 1, b: 2, c: 3 })
result.current.rerender()
})
expect(result.current.state).toEqual({ a: 1, b: 2, c: 3 })
expect(result.current.sum).toEqual(6)
act(() => {
result.current.setState({ a: 2, b: 3, c: 4 })
result.current.rerender()
})
expect(result.current.state).toEqual({ a: 2, b: 3, c: 4 })
expect(result.current.sum).toEqual(9)
If so that's good to know, I'll keep that in mind for the future.
Yeah, that should work.
@Messengerminor it doesn't look like the linked question has anything to do with this library.
That said, the answers there are correct, the way you're updating the context is triggering a render. You need a setState call somewhere to do it (the answers there have good examples already).