How to force a functional React component to render? - javascript

I have a function component, and I want to force it to re-render.
How can I do so?
Since there's no instance this, I cannot call this.forceUpdate().

🎉 You can now, using React hooks
Using react hooks, you can now call useState() in your function component.
useState() will return an array of 2 things:
A value, representing the current state.
Its setter. Use it to update the value.
Updating the value by its setter will force your function component to re-render,
just like forceUpdate does:
import React, { useState } from 'react';
//create your forceUpdate hook
function useForceUpdate(){
const [value, setValue] = useState(0); // integer state
return () => setValue(value => value + 1); // update state to force render
// A function that increment 👆🏻 the previous state like here
// is better than directly setting `setValue(value + 1)`
}
function MyComponent() {
// call your hook here
const forceUpdate = useForceUpdate();
return (
<div>
{/*Clicking on the button will force to re-render like force update does */}
<button onClick={forceUpdate}>
Click to re-render
</button>
</div>
);
}
You can find a demo here.
The component above uses a custom hook function (useForceUpdate) which uses the react state hook useState. It increments the component's state's value and thus tells React to re-render the component.
EDIT
In an old version of this answer, the snippet used a boolean value, and toggled it in forceUpdate(). Now that I've edited my answer, the snippet use a number rather than a boolean.
Why ? (you would ask me)
Because once it happened to me that my forceUpdate() was called twice subsequently from 2 different events, and thus it was reseting the boolean value at its original state, and the component never rendered.
This is because in the useState's setter (setValue here), React compare the previous state with the new one, and render only if the state is different.

Update react v16.8 (16 Feb 2019 realease)
Since react 16.8 released with hooks, function components have the ability to hold persistent state. With that ability you can now mimic a forceUpdate:
function App() {
const [, updateState] = React.useState();
const forceUpdate = React.useCallback(() => updateState({}), []);
console.log("render");
return (
<div>
<button onClick={forceUpdate}>Force Render</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.1/umd/react-dom.production.min.js"></script>
<div id="root"/>
Note that this approach should be re-considered and in most cases when you need to force an update you probably doing something wrong.
Before react 16.8.0
No you can't, State-Less function components are just normal functions that returns jsx, you don't have any access to the React life cycle methods as you are not extending from the React.Component.
Think of function-component as the render method part of the class components.

Official FAQ now recommends this way if you really need to do it:
const [ignored, forceUpdate] = useReducer(x => x + 1, 0);
function handleClick() {
forceUpdate();
}

Simplest way 👌
if you want to force a re-render, add a dummy state you can change to initiate a re-render.
const [rerender, setRerender] = useState(false);
...
setRerender(!rerender); //whenever you want to re-render
And this will ensure a re-render, And you can call setRerender(!rerender) anywhere, whenever you want :)

I used a third party library called
use-force-update
to force render my react functional components. Worked like charm.
Just use import the package in your project and use like this.
import useForceUpdate from 'use-force-update';
const MyButton = () => {
const forceUpdate = useForceUpdate();
const handleClick = () => {
alert('I will re-render now.');
forceUpdate();
};
return <button onClick={handleClick} />;
};

Best approach - no excess variables re-created on each render:
const forceUpdateReducer = (i) => i + 1
export const useForceUpdate = () => {
const [, forceUpdate] = useReducer(forceUpdateReducer, 0)
return forceUpdate
}
Usage:
const forceUpdate = useForceUpdate()
forceUpdate()

If you already have a state inside the function component and you don't want to alter it and requires a re-render you could fake a state update which will, in turn, re-render the component
const [items,setItems] = useState({
name:'Your Name',
status: 'Idle'
})
const reRender = () =>{
setItems((state) => [...state])
}
this will keep the state as it was and will make react into thinking the state has been updated

This can be done without explicitly using hooks provided you add a prop to your component and a state to the stateless component's parent component:
const ParentComponent = props => {
const [updateNow, setUpdateNow] = useState(true)
const updateFunc = () => {
setUpdateNow(!updateNow)
}
const MyComponent = props => {
return (<div> .... </div>)
}
const MyButtonComponent = props => {
return (<div> <input type="button" onClick={props.updateFunc} />.... </div>)
}
return (
<div>
<MyComponent updateMe={updateNow} />
<MyButtonComponent updateFunc={updateFunc}/>
</div>
)
}

The accepted answer is good.
Just to make it easier to understand.
Example component:
export default function MyComponent(props) {
const [updateView, setUpdateView] = useState(0);
return (
<>
<span style={{ display: "none" }}>{updateView}</span>
</>
);
}
To force re-rendering call the code below:
setUpdateView((updateView) => ++updateView);

None of these gave me a satisfactory answer so in the end I got what I wanted with the key prop, useRef and some random id generator like shortid.
Basically, I wanted some chat application to play itself out the first time someone opens the app. So, I needed full control over when and what the answers are updated with the ease of async await.
Example code:
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// ... your JSX functional component, import shortid somewhere
const [render, rerender] = useState(shortid.generate())
const messageList = useRef([
new Message({id: 1, message: "Hi, let's get started!"})
])
useEffect(()=>{
async function _ () {
await sleep(500)
messageList.current.push(new Message({id: 1, message: "What's your name?"}))
// ... more stuff
// now trigger the update
rerender(shortid.generate())
}
_()
}, [])
// only the component with the right render key will update itself, the others will stay as is and won't rerender.
return <div key={render}>{messageList.current}</div>
In fact this also allowed me to roll something like a chat message with a rolling .
const waitChat = async (ms) => {
let text = "."
for (let i = 0; i < ms; i += 200) {
if (messageList.current[messageList.current.length - 1].id === 100) {
messageList.current = messageList.current.filter(({id}) => id !== 100)
}
messageList.current.push(new Message({
id: 100,
message: text
}))
if (text.length === 3) {
text = "."
} else {
text += "."
}
rerender(shortid.generate())
await sleep(200)
}
if (messageList.current[messageList.current.length - 1].id === 100) {
messageList.current = messageList.current.filter(({id}) => id !== 100)
}
}

If you are using functional components with version < 16.8. One workaround would be to directly call the same function like
import React from 'react';
function MyComponent() {
const forceUpdate = MyComponent();
return (
<div>
<button onClick={forceUpdate}>
Click to re-render
</button>
</div>
);
}
But this will break if you were passing some prop to it. In my case i just passed the same props which I received to rerender function.

For me just updating the state didn't work. I am using a library with components and it looks like I can't force the component to update.
My approach is extending the ones above with conditional rendering. In my case, I want to resize my component when a value is changed.
//hook to force updating the component on specific change
const useUpdateOnChange = (change: unknown): boolean => {
const [update, setUpdate] = useState(false);
useEffect(() => {
setUpdate(!update);
}, [change]);
useEffect(() => {
if (!update) setUpdate(true);
}, [update]);
return update;
};
const MyComponent = () => {
const [myState, setMyState] = useState();
const update = useUpdateOnChange(myState);
...
return (
<div>
... ...
{update && <LibraryComponent />}
</div>
);
};
You need to pass the value you want to track for change. The hook returns boolean which should be used for conditional rendering.
When the change value triggers the useEffect update goes to false which hides the component. After that the second useEffect is triggered and update goes true which makes the component visible again and this results in updating (resizing in my case).

Related

React, useEfect and axios. infinite loop [duplicate]

Are there ways to simulate componentDidMount in React functional components via hooks?
For the stable version of hooks (React Version 16.8.0+)
For componentDidMount
useEffect(() => {
// Your code here
}, []);
For componentDidUpdate
useEffect(() => {
// Your code here
}, [yourDependency]);
For componentWillUnmount
useEffect(() => {
// componentWillUnmount
return () => {
// Your code here
}
}, [yourDependency]);
So in this situation, you need to pass your dependency into this array. Let's assume you have a state like this
const [count, setCount] = useState(0);
And whenever count increases you want to re-render your function component. Then your useEffect should look like this
useEffect(() => {
// <div>{count}</div>
}, [count]);
This way whenever your count updates your component will re-render. Hopefully this will help a bit.
There is no exact equivalent for componentDidMount in react hooks.
In my experience, react hooks requires a different mindset when developing it and generally speaking you should not compare it to the class methods like componentDidMount.
With that said, there are ways in which you can use hooks to produce a similar effect to componentDidMount.
Solution 1:
useEffect(() => {
console.log("I have been mounted")
}, [])
Solution 2:
const num = 5
useEffect(() => {
console.log("I will only run if my deps change: ", num)
}, [num])
Solution 3 (With function):
useEffect(() => {
const someFunc = () => {
console.log("Function being run after/on mount")
}
someFunc()
}, [])
Solution 4 (useCallback):
const msg = "some message"
const myFunc = useCallback(() => {
console.log(msg)
}, [msg])
useEffect(() => {
myFunc()
}, [myFunc])
Solution 5 (Getting creative):
export default function useDidMountHook(callback) {
const didMount = useRef(null)
useEffect(() => {
if (callback && !didMount.current) {
didMount.current = true
callback()
}
})
}
It is worth noting that solution 5 should only really be used if none of the other solutions work for your use case. If you do decide you need solution 5 then I recommend using this pre-made hook use-did-mount.
Source (With more detail): Using componentDidMount in react hooks
There's no componentDidMount on functional components, but React Hooks provide a way you can emulate the behavior by using the useEffect hook.
Pass an empty array as the second argument to useEffect() to run only the callback on mount only.
Please read the documentation on useEffect.
function ComponentDidMount() {
const [count, setCount] = React.useState(0);
React.useEffect(() => {
console.log('componentDidMount');
}, []);
return (
<div>
<p>componentDidMount: {count} times</p>
<button
onClick={() => {
setCount(count + 1);
}}
>
Click Me
</button>
</div>
);
}
ReactDOM.render(
<div>
<ComponentDidMount />
</div>,
document.querySelector("#app")
);
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
useEffect() hook allows us to achieve the functionality of componentDidMount, componentDidUpdate componentWillUnMount functionalities.
Different syntaxes of useEffect() allows to achieve each of the above methods.
i) componentDidMount
useEffect(() => {
//code here
}, []);
ii) componentDidUpdate
useEffect(() => {
//code here
}, [x,y,z]);
//where x,y,z are state variables on whose update, this method should get triggered
iii) componentDidUnmount
useEffect(() => {
//code here
return function() {
//code to be run during unmount phase
}
}, []);
You can check the official react site for more info. Official React Page on Hooks
Although accepted answer works, it is not recommended. When you have more than one state and you use it with useEffect, it will give you warning about adding it to dependency array or not using it at all.
It sometimes causes the problem which might give you unpredictable output. So I suggest that you take a little effort to rewrite your function as class. There are very little changes, and you can have some components as class and some as function. You're not obligated to use only one convention.
Take this for example
function App() {
const [appointments, setAppointments] = useState([]);
const [aptId, setAptId] = useState(1);
useEffect(() => {
fetch('./data.json')
.then(response => response.json())
.then(result => {
const apts = result.map(item => {
item.aptId = aptId;
console.log(aptId);
setAptId(aptId + 1);
return item;
})
setAppointments(apts);
});
}, []);
return(...);
}
and
class App extends Component {
constructor() {
super();
this.state = {
appointments: [],
aptId: 1,
}
}
componentDidMount() {
fetch('./data.json')
.then(response => response.json())
.then(result => {
const apts = result.map(item => {
item.aptId = this.state.aptId;
this.setState({aptId: this.state.aptId + 1});
console.log(this.state.aptId);
return item;
});
this.setState({appointments: apts});
});
}
render(...);
}
This is only for example. so lets not talk about best practices or potential issues with the code. Both of this has same logic but the later only works as expected. You might get componentDidMount functionality with useEffect running for this time, but as your app grows, there are chances that you MAY face some issues. So, rather than rewriting at that phase, it's better to do this at early stage.
Besides, OOP is not that bad, if Procedure-Oriented Programming was enough, we would never have had Object-Oriented Programming. It's painful sometimes, but better (technically. personal issues aside).
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
// Update the document title using the browser API
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
Please visit this official docs. Very easy to understand the latest way.
https://reactjs.org/docs/hooks-effect.html
Info about async functions inside the hook:
Effect callbacks are synchronous to prevent race conditions. Put the async function inside:
useEffect(() => {
async function fetchData() {
// You can await here
const response = await MyAPI.getData(someId);
// ...
}
fetchData();
}, [someId]); // Or [] if effect doesn't need props or state
useLayoutEffect hook is the best alternative to ComponentDidMount in React Hooks.
useLayoutEffect hook executes before Rendering UI and useEffect hook executes after rendering UI. Use it depend on your needs.
Sample Code:
import { useLayoutEffect, useEffect } from "react";
export default function App() {
useEffect(() => {
console.log("useEffect Statements");
}, []);
useLayoutEffect(() => {
console.log("useLayoutEffect Statements");
}, []);
return (
<div>
<h1>Hello Guys</h1>
</div>
);
}
Yes, there is a way to SIMULATE a componentDidMount in a React functional component
DISCLAIMER: The real problem here is that you need to change from "component life cycle mindset" to a "mindset of useEffect"
A React component is still a javascript function, so, if you want something to be executed BEFORE some other thing you must simply need to execute it first from top to bottom, if you think about it a function it's still a funtion like for example:
const myFunction = () => console.log('a')
const mySecondFunction = () => console.log('b)
mySecondFunction()
myFunction()
/* Result:
'b'
'a'
*/
That is really simple isn't it?
const MyComponent = () => {
const someCleverFunction = () => {...}
someCleverFunction() /* there I can execute it BEFORE
the first render (componentWillMount)*/
useEffect(()=> {
someCleverFunction() /* there I can execute it AFTER the first render */
},[]) /*I lie to react saying "hey, there are not external data (dependencies) that needs to be mapped here, trust me, I will leave this in blank.*/
return (
<div>
<h1>Hi!</h1>
</div>
)}
And in this specific case it's true. But what happens if I do something like that:
const MyComponent = () => {
const someCleverFunction = () => {...}
someCleverFunction() /* there I can execute it BEFORE
the first render (componentWillMount)*/
useEffect(()=> {
someCleverFunction() /* there I can execute it AFTER the first render */
},[]) /*I lie to react saying "hey, there are not external data (dependencies) that needs to be maped here, trust me, I will leave this in blank.*/
return (
<div>
<h1>Hi!</h1>
</div>
)}
This "cleverFunction" we are defining it's not the same in every re-render of the component.
This lead to some nasty bugs and, in some cases to unnecessary re-renders of components or infinite re-render loops.
The real problem with that is that a React functional component is a function that "executes itself" several times depending on your state thanks to the useEffect hook (among others).
In short useEffect it's a hook designed specifically to synchronize your data with whatever you are seeing on the screen. If your data changes, your useEffect hook needs to be aware of that, always. That includes your methods, for that it's the array dependencies.
Leaving that undefined leaves you open to hard-to-find bugs.
Because of that it's important to know how this work, and what you can do to get what you want in the "react" way.
const initialState = {
count: 0,
step: 1,
done: false
};
function reducer(state, action) {
const { count, step } = state;
if (action.type === 'doSomething') {
if(state.done === true) return state;
return { ...state, count: state.count + state.step, state.done:true };
} else if (action.type === 'step') {
return { ...state, step: action.step };
} else {
throw new Error();
}
}
const MyComponent = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const { count, step } = state;
useEffect(() => {
dispatch({ type: 'doSomething' });
}, [dispatch]);
return (
<div>
<h1>Hi!</h1>
</div>
)}
useReducer's dispatch method it's static so it means it will be the same method no matter the amount of times your component is re-rendered. So if you want to execute something just once and you want it rigth after the component is mounted, you can do something like the above example. This is a declarative way of do it right.
Source: The Complete Guide to useEffect - By Dan Abramov
That being said if you like to experiment with things and want to know how to do it "the imperative wat" you can use a useRef() with a counter or a boolean to check if that ref stores a defined reference or not, this is an imperative approach and it's recommended to avoid it if you're not familiar with what happen with react behind curtains.
That is because useRef() is a hook that saves the argument passed to it regardless of the amount of renders (I am keeping it simple because it's not the focus of the problem here, you can read this amazing article about useRef ). So it's the best approach to known when the first render of the component happened.
I leave an example showing 3 different ways of synchronise an "outside" effect (like an external function) with the "inner" component state.
You can run this snippet right here to see the logs and understand when these 3 functions are executed.
const { useRef, useState, useEffect, useCallback } = React
// External functions outside react component (like a data fetch)
function renderOnce(count) {
console.log(`renderOnce: I executed ${count} times because my default state is: undefined by default!`);
}
function renderOnFirstReRender(count) {
console.log(`renderOnUpdate: I executed just ${count} times!`);
}
function renderOnEveryUpdate(count) {
console.log(`renderOnEveryUpdate: I executed ${count ? count + 1 : 1} times!`);
}
const MyComponent = () => {
const [count, setCount] = useState(undefined);
const mounted = useRef(0);
// useCallback is used just to avoid warnings in console.log
const renderOnEveryUpdateCallBack = useCallback(count => {
renderOnEveryUpdate(count);
}, []);
if (mounted.current === 0) {
renderOnce(count);
}
if (mounted.current === 1) renderOnFirstReRender(count);
useEffect(() => {
mounted.current = mounted.current + 1;
renderOnEveryUpdateCallBack(count);
}, [count, renderOnEveryUpdateCallBack]);
return (
<div>
<h1>{count}</h1>
<button onClick={() => setCount(prevState => (prevState ? prevState + 1 : 1))}>TouchMe</button>
</div>
);
};
class App extends React.Component {
render() {
return (
<div>
<h1>hI!</h1>
</div>
);
}
}
ReactDOM.createRoot(
document.getElementById("root")
).render(
<MyComponent/>
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
If you execute it you will see something like this:
You want to use useEffect(), which, depending on how you use the function, can act just like componentDidMount().
Eg. you could use a custom loaded state property which is initially set to false, and switch it to true on render, and only fire the effect when this value changes.
Documentation
the exact equivalent hook for componentDidMount() is
useEffect(()=>{},[]);
hope this helpful :)

how to stop re-rendering of child component if parent update the context or state in react js?

how to stop re-rendering of child component if parent update the context or state in react js ?
I am already using React.memo still it is re-rendering.
this is my child component
const Ab = () => {
console.log("---ff-ddd");
const pageContext = useContext(PageContext);
useEffect(() => {
setTimeout(() => {
pageContext.updateGlobalMenu({});
}, 5000);
}, []);
return <div>ddd</div>;
};
export default React.memo(Ab);
I am updating the context. I want it update the context value but not re-render the child component
export default function IndexPage() {
const [globalMenu, setGlobalMenu] = useState("");
const updateGlobalMenu = (menuContent) => {
setGlobalMenu(menuContent);
};
return (
<PageContext.Provider
value={{
updateGlobalMenu
}}
>
<Ab />
</PageContext.Provider>
);
}
here is my code
https://codesandbox.io/s/friendly-bartik-3cnqvf?file=/pages/index.js:156-470
if you see it print two times console. it means it is re-rendering two times
If PageContext's value changes, then any component consuming that context (including Ab) will render. React.memo cannot stop this. In your case you're changing the value on every render, so you have room to improve it by memoizing the context value. That way, the value won't change unless it needs to:
export default function IndexPage() {
const [globalMenu, setGlobalMenu] = useState("");
const updateGlobalMenu = useCallback((menuContent) => {
setGlobalMenu(menuContent);
}, []);
const value = useMemo(() => ({
updateGlobalMenu
}), [updateGlobalMenu]);
return (
<PageContext.Provider value={value}>
<Ab />
</PageContext.Provider>
);
}
You can also, in addition to memoisation from the previous answer, split your "api" and "data" portion of the state into two different providers.
updateGlobalMenu will be in PageContextAPI provider, globalMenu will be in PageContextData provider. That way when you update the data, only the provider with the data will be re-rendered.
Take a look at this article, I covered this technique in detail here:https://www.developerway.com/posts/how-to-write-performant-react-apps-with-context

What is the point to use useCallback in a component without React.memo?

Consider example:
const Child = () => {
console.log("I did re-render!");
return null;
};
const App = () => {
const [_, setValue] = useState();
const fn = useCallback(() => {
// do something
}, []);
return (
<div>
<button onClick={() => setValue(Math.random)}>click</button>
<Child fn={fn} />
</div>
);
};
With every state change in App (click the button), the Child component re-renders, even if the passed prop fn is wrapped with useCallback. However, if I wrap Child with React.memo, it starts to work correctly - it does not re-render when parent re-renders.
My question: What's the point of using useCallbacks without React.memo?? Should I always use React.memo if I dont want the component to always re-render if its parent re-renders?
Should useCallbacks always be used with React.memo? Because it seems like they are senseless and useless without React.memo.
Playground: https://codesandbox.io/s/peaceful-khorana-nrojpb?file=/src/App.js
My question: What's the point of using useCallbacks without React.memo??
There is none unless you otherwise tell React to compare based on refernece down the line.
Should I always use React.memo if I dont want the component to always re-render if its parent re-renders?
Yes, or something equivalent.
Should useCallbacks always be used with React.memo? Because it seems like they are senseless and useless without React.memo.
Yes, unless you do something equivalent.
Just to elaborate - other than React.memo you can always wrap a sub-render with useMemo:
const App = () => {
const [_, setValue] = useState();
const fn = useCallback(() => {
// do something
}, []);
const child = useMemo(() => <Child fn={fn} />, [fn]);
return (
<div>
<button onClick={() => setValue(Math.random)}>click</button>
{child}
</div>
);
};
Or "roll your own" with useRef+useEffect or use class components and override shouldComponentUpdate or inherit from React.PureComponent.

How to subscribe on updates within ReactReduxContext.Consumer?

I would like to figure out how to subscribe on updates of a stored value it the redux store.
So far I've tried something like the following:
<ReactReduxContext.Consumer>
{({store}) => {
console.log('store:', store.getState());
const p = <p>{store.getState().value}</p>;
store.subscribe(() => {p.innerText = store.getState().value});
return p;
}}
</ReactReduxContext.Consumer>
bumping into the TypeError: can't define property "innerText": Object is not extensible error on updates.
So I wonder how to update the contents?
There are a few things about your code that are just not the way that we do things in React.
React is its own system for interacting with the DOM, so you should not attempt direct DOM manipulation through .innerText. Your code doesn't work because the variable p which you create is a React JSX Element rather than a raw HTML paragraph element, so it doesn't have properties like innerText.
Instead, you just return the correct JSX code based on props and state. The code will get updated any time that props or state change.
The ReactReduxContext is used internally by the react-redux package. Unless you have a good reason to use it in your app, I would not recommend it. There are two built-in ways that you can get a current value of state that is already subscribed to changes.
useSelector hook
(recommended)
export const MyComponent1 = () => {
const value = useSelector(state => state.value);
return <p>{value}</p>
}
connect higher-order component
(needed for class components which cannot use hooks)
class ClassComponent extends React.Component {
render() {
return <p>{this.props.value}</p>
}
}
const mapStateToProps = state => ({
value: state.value
});
const MyComponent2 = connect(mapStateToProps)(ClassComponent)
ReactReduxContext
(not recommended)
If anyone reading this has a good reason why they should need to use store.subscribe(), proper usage would look something like this:
const MyComponent3 = () => {
const { store } = useContext(ReactReduxContext);
const [state, setState] = useState(store.getState());
useEffect(() => {
let isMounted = true;
store.subscribe(() => {
if (isMounted) {
setState(store.getState());
}
});
// cleanup function to prevent calls to setState on an unmounted component
return () => {
isMounted = false;
};
}, [store]);
return <p>{state.value}</p>;
};
CodeSandbox Demo

React watch imported class property

I'm importing a plain class to my react (functional) component and want to be notified when an imported class property is set/updated. I've tried setting my imported class with just new, as a state variable with useState, as a ref with useRef - and have tried passing each one as a parameter to useEffect, but none of them are triggering the useEffect function when the property is updated a second time.
I've excluded all other code to drill down to the problem. I'm using Typescript, so my plain vanilla MyClass looks like this:
class MyClass {
userId: string
user: User?
constructor(userId: string){
this.userId = userId
// Do a network call to get the user
getUser().then(networkUser => {
// This works because I tried a callback here and can console.log the user
this.user = networkUser
}).catch(() => {})
}
}
And then in my component:
// React component
import { useEffect } from 'react'
import MyClass from './MyClass'
export default () => {
const myClass = new MyClass(userId)
console.log(myClass.user) // undefined here
useEffect(() => {
console.log(myClass.user) // undefined here and never called again after myClass.user is updated
}, [myClass.user])
return null
}
Again, this is greatly simplified. But the problem is that React is not re-rendering my component when the instance user object is updated from undefined to a User. This is all client side. How do I watch myClass.user in a way to trigger a re-render when it finally updates?
Let me guess you want to handle the business logic side of the app with OOP then relay the state back to functional React component to display.
You need a mechanism to notify React about the change. And the only way for React to be aware of a (view) state change is via a call to setState() somewhere.
The myth goes that React can react to props change, context change, state change. Fact is, props and context changes are just state change at a higher level.
Without further ado, I propose this solution, define a useWatch custom hook:
function useWatch(target, keys) {
const [__, updateChangeId] = useState(0)
// useMemo to prevent unnecessary calls
return useMemo(
() => {
const descriptor = keys.reduce((acc, key) => {
const internalKey = `##__${key}__`
acc[key] = {
enumerable: true,
configurable: true,
get() {
return target[internalKey]
},
set(value) {
if (target[internalKey] !== value) {
target[internalKey] = value
updateChangeId(id => id + 1) // <-- notify React about the change,
// the value's not important
}
}
}
return acc
}, {})
return Object.defineProperties(target, descriptor)
},
[target, ...keys]
)
}
Usage:
// React component
import { useEffect } from 'react'
import { useWatch } from './customHooks'
import MyClass from './MyClass'
export default () => {
const myClass = useMemo(() => new MyClass(userId), [userId])
useWatch(myClass, ['user'])
useEffect(() => {
console.log(myClass.user)
}, [myClass, myClass.user])
return null
}
Side Note
Not related to the question per se, but there're a few words I want to add about that myth I mentioned. I said:
props and context changes are just state change at a higher level
Examples:
props change:
function Mom() {
const [value, setValue] = useState(0)
setTimeout(() => setValue(v => v+1), 1000)
return <Kid value={value} />
}
function Dad() {
let value = 0
setTimeout(() => value++, 1000)
return <Kid value={value} />
}
function Kid(props) {
return `value: ${props.value}`
}
context change:
const Context = React.createContext(0)
function Mom() {
const [value, setValue] = useState(0)
setTimeout(() => setValue(v => v+1), 1000)
return (<Context.Provider value={value}>
<Kid />
</Context.Provider>)
}
function Dad() {
let value = 0
setTimeout(() => value++, 1000)
return (<Context.Provider value={value}>
<Kid />
</Context.Provider>)
}
function Kid() {
const value = React.useContext(Context)
return `value: ${value}`
}
In both examples, only <Mom /> can get <Kid /> to react to changes.
You can pass this.user as props and use props,.user in useEffeect. You could do that from the place getUser called.
A wholesome solution would be using a centralized state solution like redux or context API. Then you need to update store in getUser function and listen globalstate.user.
Conclusion
You need to pass this.user to the component one way or another. You need to choose according to the project.

Categories

Resources