Rerendering a React component in response to outside change without state management or resorting to context provider
Earlier this week I had to implement logging in a React Native app at work and I had a few requirements:
- Using my logger's log() method should be as easy as using console.log(), i.e. it should be possible to use it in any component regardless of where in render tree that component sits.
- Using it should not cause rerender in other components when log changes, i.e. the only component that should rerender is the one in which logs are displayed.
My first thought was to store logs in a global state, after all we were already using Redux in our app. But I wanted to get rid of Redux and I knew that most of the time we don't really need a state management library to pickup changes outside our components.
So I decided to see if I can use React's useState
and useEffect
hooks to achieve this. I went ahead and created a script in which I export a logStore object with a property for storing logs and a function for setting them:
// logStore.js
export const logStore = {
logs: ['My first log'],
setLogs: function(log) {
this.logs = [...this.logs, ...log];
}
}
This way I could simply import my script in any react component of my app and use its setLogs() method to log stuff. So far easy peasy:
// a random component
import { logStore } from '/utils/logStore.js';
export default function MyTestComponet() {
// do stuff
logStore.log(['did some stuff']);
return (
// component jsx here
)
}
Then I created the component where the logs should be displayed:
//LogPage.js
import { logStore } from '/utils/logStore.js';
export default function LogPage() {
const [logs, setLogs] = useState(logStore.logs);
return (
<View>
{logs.map((item, index) =>
<Text key={index}>{item}</Text>
)}
</View>
);
}
When LogPage component renders for the first time all is good and we can see all the logs done up to now, but if we go to any other component and do some logging in them and return the logs won't show in LogPage or if we simply log something in LogPage before the return statement (for example: logStore.setLogs(['testing']);
) it won't show. The reason is that when logs property of logStore object changes our component's local state doesn't change so our component doesn't get rerendered. A react component only gets rerendered when its prop or state changes. In frameworks like Angular you can subscribe to an observable, but in React we need to use a hook called useEffect
to update the local state. The code inside a useEffect
doesn't run during rerenders, so we can update the state inside it without causing an endless loop of rerendering. Here is the updated version:
//LogPage.js
import { logStore } from './logStore.js';
export default function LogPage() {
const [logs, setLogs] = useState(logStore.logs);
useEffect(() => {
setLogs([...logStore.logs]);
}, [logStore.logs]);
return (
<View>
{logs.map((item, index) =>
<Text key={index}>{item}</Text>
)}
</View>
);
}
Note that it's important to provide logStore.logs
in useEffect's dependency array.