Nested react hooks exposing static references - reactjs

I was looking the implementation of eslint-plugin-react-hooks and it looks like useState's setState and useReducer's dispatch functions are static references which are not required to be declared in the dependency array in useEffect.
However, this rule does not seem to work properly when you write a custom rule that abstracts an internal logic. for example, I create a custom hook:
const useCustom = () => {
const [number, setNumber] = React.useState(0);
return [number, setNumber];
};
which is then used the following way:
const [number, setNumber] = useCustom();
React.useEffect(() => {
if (something) {
setNumber(1);
}
}, [something]); // useEffect has a missing dependency: 'setNumber'
Adding the dependency to the array does not seem to cause extra render cycles. However, it raises a another question: What if my hook returns a reference to an value returned from useRef() would that be safe to add it to the dependencies array?

The value from useRef will never change unless you unmount and remount the component.
Changing value.current will not cause the component to re render but if the value.current is a dependency of your effect then your effect will re run when the component re renders.
Not sure if this answers your question, I added sample code below to demonstrate:
const useTimesRendered = () => {
const rendered = React.useRef(0);
rendered.current++;
return rendered;
};
const useCustomRef = () => {
const customRef = React.useRef(0);
return customRef;
};
const App = () => {
const rendered = useTimesRendered();
const custom = useCustomRef();
const [, reRender] = React.useState({});
React.useEffect(
() => console.log('rendered ref only on mount', rendered.current),
[rendered]
);
const customVal = custom.current;
React.useEffect(
() => console.log('custom ref.current only when re rendering', customVal),
[customVal]
);
return (
<div>
<h1>Times rendered: {rendered.current}</h1>
<button onClick={() => custom.current++}>
increase custom
</button>
<button onClick={() => reRender({})}>
Re render app
</button>
</div>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

If I've understood your question correctly, than will go with yes. If ref never changes (capturing some node on component mount), or if it changes frequetly (like in usePrevious hook) it should be added to the list of dependencies. Only potential issue I could think of is when someone, for example use ref on input and than expect that will get a new updated ref after user type somenthing. But that is a topic for some other time

Related

react function component issue with usEffect and useState

Sometimes I have to use some native js libaray api, So I may have a component like this:
function App() {
const [state, setState] = useState(0)
useEffect(() => {
const container = document.querySelector('#container')
const h1 = document.createElement('h1')
h1.innerHTML = 'h1h1h1h1h1h1'
container.append(h1)
h1.onclick = () => {
console.log(state)
}
}, [])
return (
<div>
<button onClick={() => setState(state => state + 1)}>{state}</button>
<div id="container"></div>
</div>
)
}
Above is a simple example. I should init the lib after react is mounted, and bind some event handlers. And the problem is coming here: As the above shown, if I use useEffect() without state as the item in dependencies array, the value state in handler of onclick may never change. But if I add state to dependencies array, the effect function will execute every time once state changed. Above is a easy example, but the initialization of real library may be very expensive, so that way is out of the question.
Now I find 3 ways to reslove this, but none of them satisfy me.
Create a ref to keep state, and add a effect to change it current every time once state changed. (A extra variable and effect)
Like the first, but define a variable out of the function instead of a ref. (Some as the first)
Use class component. (Too many this)
So is there some resolutions that solve problems and makes code better?
I think you've summarised the options pretty well. There's only one option i'd like to add, which is that you could split your code up into one effect that initializes, and one effect that just changes the onclick. The initialization logic can run just once, and the onclick can run every render:
const [state, setState] = useState(0)
const h1Ref = useRef();
useEffect(() => {
const container = document.querySelector('#container')
const h1 = document.createElement('h1')
h1Ref.current = h1;
// Do expensive initialization logic here
}, [])
useEffect(() => {
// If you don't want to use a ref, you could also have the second effect query the dom to find the h1
h1ref.current.onClick = () => {
console.log(state);
}
}, [state]);
Also, you can simplify your option #1 a bit. You don't need to create a useEffect to change ref.current, you can just do that in the body of the component:
const [state, setState] = useState(0);
const ref = useRef();
ref.current = state;
useEffect(() => {
const container = document.querySelector('#container');
// ...
h1.onClick = () => {
console.log(ref.current);
}
}, []);

React linter suggesting to wrap constant array with useMemo inside useEffect react-hooks/exhaustive-deps warning

React linter is giving a warning:
The 'someUnchangingArray' array makes the dependencies of useEffect Hook (at line 42) change on every render. Move it inside the useEffect callback. Alternatively, wrap the initialization of 'someUnchangingArray' in its own useMemo() Hook react-hooks/exhaustive-deps
Why? someUnchangingArray is a constant and wont change, why is React linter suggesting that it will trigger the useEffect on every render?
export default function Component(
const [initialized, setInitialized] = useState(false);
const someUnchangingArray = ["1", ""];
useEffect(() => {
if (!initialized) {
// do some iniitializing
console.log(someUnchangingArray, initialized);
setInitialized(true);
}
}, [someUnchangingArray, initialized]);
...
v-dom rerender basically means it will rerun all your component(function)
hence you will create new array reference for every render
simply move the constant outside
const someUnchangingArray = ["1", ""];
export default function Component(
const [initialized, setInitialized] = useState(false);
useEffect(() => {
if (!initialized) {
// do some iniitializing
console.log(someUnchangingArray, initialized);
setInitialized(true);
}
}, [someUnchangingArray, initialized]);
...
or for whatever reason it need to be inside(rely on your state), then you need the useMemo hook
Your component is a function that is called on every render.
It means that on every render someUnchangingArray will point to a newly created reference of an array.
useEffect will receive a new reference and will decide that it's time to be called.
function render() {
const someUnchangingArray = ["1", ""];
return someUnchangingArray
}
console.log(render() === render()); // false
Here is an illustration:
function Component() {
const [counter, setCounter] = React.useState(0);
const supposedlySomeUnchangingArray = ["1", ""];
React.useEffect(() => {
console.count('USE EFFECT HAS BEEN CALLED');
}, [supposedlySomeUnchangingArray]);
return (
<div>
{counter}
<button onClick={() => setCounter(counter + 1)}>do something to rerender</button>
</div>
);
}
ReactDOM.render(<Component />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root">
</div>
Check the console and you will see that useEffect is called on each rerender.

Is this useEffect() unecessarily expensive?

This is a very simple version of my code:
const MyComponent = (props)=>{
const [randVar, setRandVar] = useState(null);
const randomFunction = ()=>{
console.log(randVar, props);
};
useCustomHook = ()=>{
useEffect(()=>{
document.addEventListener('keydown', randomFunction);
return ()=>{
document.removeEventListener('keydown', randomFunction);
}
}, [props, randVar]);
}
useCustomHook();
...
};
I want randomFunction to log accurate values for randVar and props (i.e. logs update when those variables change values), but I'm concerned that adding an event listener and then dismounting it every time they change is really inefficient.
Is there another way to get randomFunction to log updated values without adding props and randVar as dependencies in useEffect?
A few things, your useEffect does not need to be in that custom hook at all... It should probably look like this:
const MyComponent = (props)=>{
const [randVar, setRandVar] = useState(null);
const randomFunction = useCallback(()=>{
console.log(randVar, props);
}, [props, randVar]);
useEffect(()=>{
document.addEventListener('keydown', randomFunction);
return ()=>{
document.removeEventListener('keydown', randomFunction);
}
}, [randomFunction]);
...
};
useCallback will keep your function from being redefined on every render, and is the correct dependency for that useEffect as well. The only thing bad about the performance ehre is that you are logging props so it needs to be in the dependency array of the useCallback and since it is an object that may get redefined a lot, it will cause your useCallback to get redefined on nearly every render, which will then cause your useEffect to be fired on nearly every render.
My only suggestion there would be to separate your logging of props from where you log changes to randVar.
I don't think you need randVar as a dependency and if you had ESLint, it would tell you the same since you never acutally reference randVar in the effect.
If you don't want the function to get rebuilt over and over, you need to either memoize it or useRef. Unfortunately, once it's a ref it's not reactive. Maybe I'm overthinking this, but you could have a ref that you update in an effect with the new value and print out the value of that ref in the callback you pass to randFunction. See my example below.
I don't like how I did this, but it doesn't remake the function. I'm trying to think how I could do it better, but I think this works how you want.
const {
useRef,
useState,
useEffect
} = React;
const useCustomHook = (fn) => {
useEffect(() => {
document.addEventListener("keydown", fn);
return () => {
document.removeEventListener("keydown", fn);
};
}, [fn]);
};
const MyComponent = (props) => {
const [randVar, setRandVar] = useState("");
const randVarRef = useRef("");
useEffect(() => {
randVarRef.current = randVar;
}, [randVar]);
const randFn = useRef(() => {
console.log(randVarRef.current);
});
useCustomHook(randFn.current);
return React.createElement("input", {
type: "text",
value: randVar,
onChange: e => {
setRandVar(e.target.value);
}
});
};
ReactDOM.render(
React.createElement(MyComponent),
document.getElementById("app")
);
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<div id="app"></div>

React Hooks Wrapper not getting updated after state change in useEffect

Current behaviour
I'm using a functional component with a setState hook in useEffect. The state variable that is set inside useEffect is wrapped over the return statement to render the JSX for the component.
When I debug into it, the component renders with the correct state variable but my wrapper in my test does Not show the correct information.
wrapper.update() isn't fixing this issue.
Below is a snippet of what I am trying to achieve:
const DummyComponent= ({}) => {
const [selected, setSelected] = React.useState(false);
useEffect(() => {
setSelected(true)
}, [someDependency])
return (
{
selected && (
<div id= 'container'>
{childComponents}
</div>)
}
);
})
it('test', () => {
const wrapper= mount( <DummyComponent /> );
wrapper = wrapper.update(); // this doesn't fix my problem
wrapper.find('#container')first().props().onClick();
expect(wrapper.toMatchSnapshot());
});
I am getting the below error:
Method “props” is meant to be run on 1 node. 0 found instead.
Expected Behaviour
After state update in useEffect re-render should be triggered in test case and element with id="container" should be found.
Note: This is not same as https://github.com/enzymejs/enzyme/issues/2305
 
It seems to me there's some other problem with your real code (maybe some promise-based code invoked in the effect?). Here's a working example based on your snippet:
const DummyComponent = ({}) => {
const [selected, setSelected] = React.useState(false);
const [result, setResult] = React.useState("");
React.useEffect(() => {
setSelected(true);
}, []);
return selected && <div id='container' onClick={() => setResult("test")}>
<label>{result}</label>
</div>;
};
it('test', () => {
const wrapper = mount(<DummyComponent/>);
act(() => {
wrapper.find('#container').first().props().onClick();
});
expect(wrapper.find("label").text()).toEqual("test");
});
The act is actually needed only for interaction with the component itself, not for after-render effect.
The problem is that when you first mount the component, it does not render anything, because selected is false. So, when you search for '#container', you don't get anything.
If the update is enough, then it should probably be executed before the wrapper.find(), so that the component is rendered with selected true. But React is asynchronous and I suspect that this will not be enough…
I fixed my problem, actually I need to assign my component to a different wrapper and then update the wrapper and then check for updates on the wrapper instead of the component. Below is the snippet:
it('test', () => {
const component= mount( <DummyComponent /> );
const wrapper = component.update();
wrapper.find('#container')first().props().onClick();
expect(wrapper.toMatchSnapshot());
});
This will have the updated component

React rerenders my memoïzed component when I use an HOC in the parent

I am having an issue with an Image blinking because it is rendered for no reason, despite using React.memo, and despite non of it's props or state being changed.
I succeeded here to make the correct use of React.memo to make this work, BUUUT, for a reason that I don't understand, if I use an High Order Component within the Parent component, memo doesn't work anymore and I get my blinking issue again.
Here is a snack that illustrates the problem.
Here is the code:
import * as React from 'react';
import { Text, View, StyleSheet } from 'react-native';
let interval = null
const Icon = ({ name }) => {
// We emulate a rerender of the Icon by logging 'rerender' in the console
console.log('rerender')
return <Text>{name}</Text>
}
const Memo = React.memo(Icon, () => true)
const withHOC = (Comp) => (props) => {
return <Comp {...props}/>
}
export default function App() {
const [state, setState] = React.useState(0)
const name = 'constant'
// Change the state every second
React.useEffect(() => {
interval = setInterval(() => setState(s => s+1), 1000)
return () => clearInterval(interval)
}, [])
// Remove this line and replace NewView by View to see the expected behaviour
const NewView = withHOC(View)
return (
<NewView>
<Memo name={name} />
</NewView>
);
}
I don't understand why my HOC breaks the memoization, and I have no idea how to prevent the blinking in my app and still be able to use HOC...
You're re-creating the HOC within your render function. Because of this React can't keep any of that component's children consistent between renders.
If you move the HOC creation outside of the render, then it'll work!
const Text = 'span';
const View = 'div';
let interval = null
const Icon = ({ name }) => {
// We emulate a rerender of the Icon by logging 'rerender' in the console
console.log('rerender')
return <Text>{name}</Text>
}
const Memo = React.memo(Icon, () => true)
const withHOC = (Comp) => (props) => {
return <Comp {...props}/>
}
// move it out here!
// 👇👇👇
const NewView = withHOC(View)
// 👆👆👆
function App() {
const [state, setState] = React.useState(0)
const name = 'constant'
// Change the state every second
React.useEffect(() => {
interval = setInterval(() => setState(s => s+1), 1000)
return () => clearInterval(interval)
}, [])
// Remove this line and replace NewView by View to see the expected behaviour
return (
<NewView>
<Memo name={name} />
</NewView>
);
}
ReactDOM.render(<App />, document.querySelector('#root'));
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root"></div>
Edit: I saw your comment in the other answer.
Ok, but how can I use the HOC within my component? Because I need to provide the props and the state to the hoc...
If you do need to create the HOC within the component, you can wrap it with useMemo and that will also work because React will preserve your HOC reference between renders if the dependencies of the useMemo don't change (note: this will not work if your hook dependencies change for every render).
function App() {
// ...
const NewView = useMemo(() => withHOC(View), []);
}
Although this works, it can be kind of wonky. In general, hooks and HOCs are not patterns to be used together. The React core team created hooks to replace HOCs. Before you continue down that road, I would attempt to see if you can write your HOC as a hook. I think you'll find that it's a lot more natural.
On every re-render you create a new NewView so the old one (along with your Icon) will be destroyed for the new one. So, it wasn't actually a re-render that was happening on the Icon, but a totally new render of a new Icon.
If you move const NewView = withHOC(View) outside your App function, your HOC will be called once, creating a NewView that will be used on every re-render and this will prevent your Icon from also being destroyed and as you have it memoized, you are safe from unnecessary re-renders.
import * as React from 'react';
import { Text, View, StyleSheet } from 'react-native';
let interval = null
const Icon = ({ name }) => {
// We emulate a rerender of the Icon by logging 'rerender' in the console
console.log('rerender')
return <Text>{name}</Text>
}
const Memo = React.memo(Icon, () => true)
const withHOC = (Comp) => (props) => {
return <Comp {...props}/>
}
const NewView = withHOC(View);
export default function App() {
const [state, setState] = React.useState(0)
const name = 'constant'
// Change the state every second
React.useEffect(() => {
interval = setInterval(() => setState(s => s+1), 1000)
return () => clearInterval(interval)
}, [])
// Remove this line and replace NewView by View to see the expected behaviour
return (
<NewView>
<Memo name={name} />
</NewView>
);
}
To better understand what's happening, I added a log here on your Icon component so you can see that the component unmounts on every parent re-render, while it's forced to be destroyed by the creation of a totally new NewView with a new memoized Icon.

Resources