I've got some components which need to render sequentially once they've loaded or marked themselves as ready for whatever reason.
In a typical {things.map(thing => <Thing {...thing} />} example, they all render at the same time, but I want to render them one by one I created a hook to to provide a list which only contains the sequentially ready items to render.
The problem I'm having is that the children need a function in order to tell the hook when to add the next one into its ready to render state. This function ends up getting changed each time and as such causes an infinite number of re-renders on the child components.
In the examples below, the child component useEffect must rely on the dependency done to pass the linter rules- if i remove this it works as expected because done isn't a concern whenever it changes but obviously that doesn't solve the issue.
Similarly I could add if (!attachment.__loaded) { into the child component but then the API is poor for the hook if the children need specific implementation such as this.
I think what I need is a way to stop the function being recreated each time but I've not worked out how to do this.
Codesandbox link
useSequentialRenderer.js
import { useReducer, useEffect } from "react";
const loadedProperty = "__loaded";
const reducer = (state, {i, type}) => {
switch (type) {
case "ready":
const copy = [...state];
copy[i][loadedProperty] = true;
return copy;
default:
return state;
}
};
const defaults = {};
export const useSequentialRenderer = (input, options = defaults) => {
const [state, dispatch] = useReducer(options.reducer || reducer, input);
const index = state.findIndex(a => !a[loadedProperty]);
const sliced = index < 0 ? state.slice() : state.slice(0, index + 1);
const items = sliced.map((item, i) => {
function done() {
dispatch({ type: "ready", i });
return i;
}
return { ...item, done };
});
return { items };
};
example.js
import React, { useEffect, useState } from "react";
import ReactDOM from "react-dom";
import { useSequentialRenderer } from "./useSequentialRenderer";
const Attachment = ({ children, done }) => {
const [loaded, setLoaded] = useState(false);
useEffect(() => {
const delay = Math.random() * 3000;
const timer = setTimeout(() => {
setLoaded(true);
const i = done();
console.log("happening multiple times", i, new Date());
}, delay);
return () => clearTimeout(timer);
}, [done]);
return <div>{loaded ? children : "loading"}</div>;
};
const Attachments = props => {
const { items } = useSequentialRenderer(props.children);
return (
<>
{items.map((attachment, i) => {
return (
<Attachment key={attachment.text} done={() => attachment.done()}>
{attachment.text}
</Attachment>
);
})}
</>
);
};
function App() {
const attachments = [1, 2, 3, 4, 5, 6, 7, 8].map(a => ({
loaded: false,
text: a
}));
return (
<div className="App">
<Attachments>{attachments}</Attachments>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Wrap your callback in an aditional layer of dependency check with useCallback. This will ensure a stable identity across renders
const Component = ({ callback }) =>{
const stableCb = useCallback(callback, [])
useEffect(() =>{
stableCb()
},[stableCb])
}
Notice that if the signature needs to change you should declare the dependencies as well
const Component = ({ cb, deps }) =>{
const stableCb = useCallback(cb, [deps])
/*...*/
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With