React useContext & useEffect show stale data - reactjs

https://codesandbox.io/s/react-usecontextuseeffect-stale-data-bug-l81sn
In a component I use useEffect to do something when a certain value changes. Now it's only the value of a simple counter, but in the real world it would be a array where items are removed or added etc. A little bit more complex.
In the useEffect I also have a resize detection. Again, in this example not very interesting.
const App = props => {
const [count, dispatch] = useReducer((state, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
}, 0);
return (
<CountContext.Provider value={{ count, dispatch }}>
<div className="App">
<h1>App</h1>
<Counter />
</div>
</CountContext.Provider>
);
};
const Counter = () => {
const counter = useContext(CountContext);
useEffect(() => {
window.addEventListener('resize', () => {
console.log(counter.count);
})
},[counter])
return (
<div className="Counter">
<p>Counter: {counter.count}</p>
<input
type="button"
value="+"
onClick={() => counter.dispatch({ type: 'INCREMENT' })}
/>
<input
type="button"
value="-"
onClick={() => counter.dispatch({ type: 'DECREMENT' })}
/>
</div>
);
};
The issue is that when I resize the viewport the console.log(counter.count) shows all previous values:

The issue is a memory leak in the useEffect() method. You need to clean up on re-renders. I forked your sandbox and tested it with this and it works as expected:
useEffect(() => {
const resizeEvent = () => {
console.log(counter.count);
};
window.addEventListener("resize", resizeEvent);
return () => {
window.removeEventListener("resize", resizeEvent);
};
}, [counter]);
Notice the return for the clean up and the refactor of your code to a called function so that it can be correctly removed on dismount.

You add a new one eventListener every time your state changes. Three changes - three eventListeners. Moreover, when your Counter component is unmounted, the listeners keep alive that cause memory leaks.
First of all, you can take this part outside of useEffect:
window.addEventListener('resize', () => {
console.log(counter.count);
})
Or you should use empty array as dependencies list in useEffect, then it will fire just once:
useEffect(() => {
}, []) // empty array here says 'do it once'
And finally, useEffect is a perfect place for fetching data or subscribing for events etc. But do not forget to clear all it up after component is not needed anymore. For doing this, return cleaning function in useEffect:
useEffect(() => {
// your main logic here
...
// cleaning up function:
return () => {
removeEventListener, unsubscribe etc...
}
}, [])

Related

Best approach in updating a useReducer state in reaction to a change in its initialArg

After the first render, the useReducer hook doesn't react to changes in its initialArg (second positional) argument. It makes it hard to properly sync it with an external value, without having to rely on an extra cycle by dispatching a reset action inside a useEffect hook.
I built a minimal example. It's a simple, formik-like, form provider. Here's what it looks like:
// App.js
const users = {
1: {
firstName: 'Paul',
lastName: 'Atreides',
},
2: {
firstName: 'Duncan',
lastName: 'Idaho',
},
};
const App = () => {
const [id, setId] = useState(1);
return (
<>
<div>Pick User</div>
<button onClick={() => { setId(1); }} type="button">User 1</button>
<button onClick={() => { setId(2); }} type="button">User 2</button>
<FormProvider initialValues={users[id]}>
<Editor />
</FormProvider>
</>
);
};
// FormProvider.js
const reducer = (state, action) => {
switch (action.type) {
case 'UPDATE_FIELD':
return { ...state, [action.field]: action.value };
default:
throw new Error();
}
};
const FormProvider = ({ children, initialValues }) => {
const [values, dispatch] = useReducer(reducer, initialValues);
const handleChange = useCallback((evt) => {
dispatch({
field: evt.target.name,
type: 'UPDATE_FIELD',
value: evt.target.value,
});
}, []);
return (
<FormContext.Provider value={{ handleChange, values }}>
{children}
</FormContext.Provider>
);
};
// Editor.js
const Editor = () => {
const { handleChange, values } = useContext(FormContext);
return (
<>
<div>First name:</div>
<input
name="firstName"
onChange={handleChange}
value={values.firstName}
/>
<div>First name:</div>
<input
name="lastName"
onChange={handleChange}
value={values.lastName}
/>
</>
);
};
If you open the demo and click on the User 2 button, you'll notice that nothing happens. It's not surprising since we know that the useReducer hook gets initialised once using the provided initialArg argument and never reads its value again.
What I expect is the useReducer state to reflect the new initialArg prop, i.e. I want to see "Duncan" in the First name input after clicking on the User 2 button.
From my point of vue, I can see two options:
1. Passing a key prop to the FormProvider component.
// App.js
const App = () => {
// ...
return (
<>
{/* ... */}
<FormProvider key={id} initialValues={users[id]}>
<Editor />
</FormProvider>
</>
);
};
This will indeed fix the problem by destroying and re-creating the FormProvider component (and its children) every time the id changes. But it feels like a hack to me. Plus, it seems inefficient to rebuild that entire part of the tree (which is substantial in the real application) just to get that input values updated. However, this seems to be a common fix for such problems.
2. Dispatch a RESET action whenever initialValues changes
// FormProvider.js
const reducer = (state, action) => {
switch (action.type) {
case 'UPDATE_FIELD':
return { ...state, [action.field]: action.value };
case 'RESET':
return action.values;
default:
throw new Error();
}
};
const FormProvider = ({ children, initialValues }) => {
// ...
const isFirstRenderRef = useRef(true);
useEffect(() => {
if (!isFirstRenderRef.current) {
dispatch({
type: 'RESET',
values: initialValues,
});
}
}, [initialValues]);
useEffect(() => {
isFirstRenderRef.current = false;
}, []);
// ...
};
This will work as well, but, because it's happening inside a useEffect hook, it will require an extra cycle. It means that there'll be a moment where the form will contain stale values. If the user types at that moment, it could cause a race condition.
3. Idea
I read in this article by Mark Erikson that:
Function components may call setSomeState() directly while rendering, as long as it's done conditionally and isn't going to execute every time this component renders. [...] If a function component queues a state update while rendering, React will immediately apply the state update and synchronously re-render that one component before moving onwards.
So it seems that I should be able to call dispatch({ type: RESET, values: initialValues }); directly from the body of the function, under the condition that initialValues did change (I'd use a ref to keep track of its previous value). This should result in the state being updated in just one cycle. However, I couldn't get this to work.
——
What do you think is best between option 1, 2 and (3). Any advice/guidance on how I should address this problem?

React re-renders entire list of components even with unique keys

I am using the React useState hook to update a list of items. I would like for only the added/updated components to be rendered but everytime the state of of the list changes all the items in list are re-rendered.
I have followed Preventing list re-renders. Hooks version. to solve the re-render issue but it doesn't work
Can someone help me understand, what's wrong with the below code or if this is actually not the right way to do it
function App() {
const [arr, setArr] = useState([])
useEffect(() => {
//getList here returns a list of elements of the form {id: number, name: string}
setArr(getList());
}, [])
const clickHandle = useCallback((e, id) => {
e.preventDefault()
setArr((arr) => {
return [...arr, {
id: id + 100,
name: `test${id+100}`
}]
})
}, [arr])
return (
<div className="App">
{
arr.map((item) => {
return (
<NewComp key={`${item.id}`} item={item} clickHandle={clickHandle} />
);
})
}
</div>
);
}
const NewComp = ({
item,
clickHandle
}) => {
return (
<div>
<button onClick={(e) => clickHandle(e, item.id)}>{item.name}</button>
</div>
);
}
The reason all your NewComp re-render is because your clickHandle function is being recreated whenever there is any change in the state arr.
This happens because you have added arr as a dependency to useCallback. This however is not required.
Once you fix it, you can wrap your NewComp with React.memo to optimize their re-renders. Also you must note that call the render function of a component is different from actually re-rendering it in the DOM.
const clickHandle = useCallback((e, id) => {
e.preventDefault()
setArr((arr) => {
return [...arr, {
id: id + 100,
name: `test${id+100}`
}]
})
}, []);
const NewComp = React.memo({
item,
clickHandle
}) => {
return (
<div>
<button onClick={(e) => clickHandle(e, item.id)}>{item.name}</button>
</div>
);
});

Set State only once

I am using a function (getActorInfo()) in react to grab info from an api and set that in a State. It works but the function wont stop running.
export default function ActorProfile({ name, img, list, id, getActorInfo }) {
const [showList, setShowList] = useState(false);
const [actorInfo, setActorInfo] = useState({});
getActorInfo(id).then(val => setActorInfo(val));
console.log(actorInfo)
return (
<Wrapper>
<Actor
id={id}
name={name}
img={img}
onClick={() => {
setShowList(!showList);
}}
actorBirthday={actorInfo.actorBirthday}
/>
{showList && <MovieList list={list} actorInfo={actorInfo} />}
</Wrapper>
);
}
I tried using useEffect like this
useEffect(() => {
getActorInfo(id).then(val => setActorInfo(val));
}, {});
But I get an error that I do not understand
Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
in ActorProfile (at App.js:60)
My question is how to have this function only run once?
Anything in a functional component body will run every render. Changing to a useEffect is the correct solution to this problem.
It isn't working for you because useEffect takes an array as its second parameter, not an object. Change it to [], and it will only run once.
useEffect(() => {
getActorInfo(id).then(val => setActorInfo(val));
}, []);
This will be equivalent to the class-based componentDidMount.
If your hook has a dependency, you add it to the array. Then the effect will check to see if anything in your dependency array has changed, and only run the hook if it has.
useEffect(() => {
// You may want to check that id is truthy first
if (id) {
getActorInfo(id).then(val => setActorInfo(val));
}
}, [id]);
The resulting effect will be run anytime id changes, and will only call getActorInfo if id is truthy. This is an equivalent to the class-based componentDidMount and componentDidUpdate.
You can read more about the useEffect hook here.
You are still not checking if the component is mounted before you set the state. You can use a custom hook for that:
const useIsMounted = () => {
const isMounted = useRef(false);
useEffect(() => {
isMounted.current = true;
return () => (isMounted.current = false);
}, []);
return isMounted;
};
Then in your component you can do:
const isMounted = useIsMounted();
useEffect(() => {
getActorInfo(id).then(
val => isMounted && setActorInfo(val)
);
}, [getActorInfo, id, isMounted]);
you need to cleanup useEffect like
useEffect(() => {
getActorInfo(id).then(val => setActorInfo(val));
return () => {
setActorInfo({});
}
},[]);
have a look at this article. It explains you why to cleanup useEffect.

State property updated value cannot be accessed in onClick function

I'm using React Hooks. I set the state property questions after an axios fetch call. Now when I click a button, in its function questions state is still empty
const [questions, setQuestions] = useState([]);
const [customComponent, setCustomComponent] = useState(<div />);
useEffect(() => {
axios.get("urlhere").then(res => {
console.log(12, res.data);
setQuestions(res.data);
res.data.map(q => {
if (q.qualifyingQuestionId == 1) {
setCustomComponent(renderSteps(q, q.qualifyingQuestionId));
}
});
});
}, []);
const handleNext = i => {
console.log(32, questions); //questions is still an empty array here
};
const renderSteps = (step, i) => {
switch (step.controlTypeName) {
case "textbox":
return (
<div key={i}>
<input type="text" placeholder={step.content} />
<button onClick={() => handleNext(i)}>Next</button>
</div>
);
}
};
return <>{customComponent}</>;
Do I need to use reducers here and put the custom component in another "file"?
setQuestions does not update state immediately, you should use the prevState instead to access the new value.
Here's a sandbox to match your codes with some explanation on why it was empty > https://codesandbox.io/s/axios-useeffect-kdgnw
You can also read about it here: Why calling react setState method doesn't mutate the state immediately?
Finally I have my own solution
I passed down the data from the fetch function to another component as props
useEffect(() => {
axios.get('url')
.then((data) => {
setCustomComponent(<Questions questions={data} />)
})
}, [])

changes to state issued from custom hook not causing re-render even though added to useEffect

I have a custom hook that keeps a list of toggle states and while I'm seeing the internal state aligning with my expectations, I'm wondering why a component that listens to changes on the state kept by this hook isn't re-rendering on change. The code is as follows
const useToggle = () => {
const reducer = (state, action) => ({...state, ...action});
const [toggled, dispatch] = useReducer(reducer, {});
const setToggle = i => {
let newVal;
if (toggled[i] == null) {
newVal = true;
} else {
newVal = !toggled[i];
}
dispatch({...toggled, [i]: newVal});
console.log('updated toggled state ...', toggled);
};
return {toggled, setToggle};
};
const Boxes = () => {
const {setToggle} = useToggle()
return Array.from({length: 8}, el => null).map((el,i) =>
<input type="checkbox" onClick={() => setToggle(i)}/>)
}
function App() {
const {toggled} = useToggle()
const memoized = useMemo(() => toggled, [toggled])
useEffect(() => {
console.log('toggled state is >>>', toggled) // am not seeing this on console after changes to toggled
}, [toggled])
return (
<div className="App">
<Boxes />
</div>
);
}
It's because you are using useToggle twice.
once in the App
another one in the Boxes.
When you dispatch the action in Boxes, it's updating the toggled instance for Boxes (which is not retrieved in it).
Think of your custom hook like how you use useState. When you use useState, each component gets its own state. Same goes for the custom hook.
So there are a few ways you can address the issue.
Pass the setToggle from App to Boxes via prop-drilling
Use Context API (or Redux or other statement management library to pass
setToggle instance in the App component down)
Here is an example of prop-drilling.
You can follow along
const Boxes = ({ setToggle }) => {
// const { setToggle } = useToggle();
return Array.from({ length: 8 }, el => null).map((el, i) => (
<input key={i} type="checkbox" onClick={() => setToggle(i)} />
));
};
function App() {
const { toggled, setToggle } = useToggle();
useEffect(() => {
console.log("toggled state is >>>", toggled); // am not seeing this on console after changes to toggled
}, [toggled]);
return (
<div className="App">
<Boxes setToggle={setToggle} />
</div>
);
}
Note: that I added key props in Boxes using the index i(and it is a bad practice by the way)
You can see that it's now working as you'd expect.

Resources