How to conditionally render a React component that uses hooks - reactjs

The following (albeit contrived) component violates react-hooks/rules-of-hooks (via eslint)
function Apple(props){
const {seeds} = props;
if(!seeds){
return null;
}
const [bitesTaken, setBitesTaken] = useState(0);
return <div>{bitesTaken}</div>
}
with the following error
React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return?
I understand that hooks need to be called in the same order, but this kind of conditional rendering (so long as no hooks are invoked before the return, does not prevent the hooks from being called in the same order. I think the eslint rule is too strict, but maybe there's a better way.
What is the best way to conditionally render a component which uses hooks?

Another option is to split the component.
import React,{useState} from 'react';
const Apple = (props) => {
const {seeds} = props;
return !!seeds && <AppleWithSeeds />;
};
const AppleWithSeeds =(props) => {
const [bitesTaken] = useState(0);
return <div>{bitesTaken}</div>
};
export default Apple;
Advantage of this method is that your components stay small and logical.
And in your case you might have something more than '0' in the useState initializer which you don't want unnecessary at the top before the condition.

You can't conditionally use hooks, so move the hook above the condition.
function Apple(props){
const {seeds} = props;
const [bitesTaken, setBitesTaken] = useState(0);
if(!seeds){
return null;
}
return <div>{bitesTaken}</div>
}
You can also simplify the rendering like this:
function Apple(props) {
const { seeds } = props
const [bitesTaken, setBitesTaken] = useState(0)
return !!seeds && <div>{bitesTaken}</div>
}
If seeds is falsey, then false will be returned (which renders as nothing), otherwise, the div will be returned.
I've added the double exclamation point to seeds to turn it into a boolean, because if seeds is undefined then nothing is returned from render and React throws an error.

Related

Which is the correct way to handle values props before charge component in React?

i have a doubt about React hook, his props and useEffect.
In my component, i receive a list inside props, i must filter the received list and get a value from the list before the component is mounted.
(this code is a simplified example, i apply another filters and conditions more complex)
function MyComponent(props) {
const [value, setValue] = useState(null)
useEffect(()=>{
var found = props.myList.find((x)=> {return x.name=="TEST"});
if(found.length > 0)
setValue("Hello World")
}, []);
return (
<div>{value}</div>
)
}
Is correct to use useEffect for get and set a value respect to props before the component is mounted?
This useEffect is executed after the 1st render, so null would be "rendered" first, and then the useEffect would update the state causing another render.
However, if your value is dependent on the props, and maybe on other states (filters for example), this is a derived value, and it doesn't need to be used as a state. Instead calculate it on each render, and if it's a heavy computation that won't change on every render wrap it with useMemo:
function MyComponent({ myList }) {
const value = useMemo(() => {
const found = myList.some(x => x.name=="TEST");
return found ? 'Hello World' : '';
}, [myList]);
return (
<div>{value}</div>
)
}
There's no need to use useEffect here, you can simply pass a callback to the useState hook. The callback is only ran on the initialization of the hook so this has the same outcome as your useEffect hook (i.e. it doesn't run on every rerender so same performance hit).
function MyComponent({ myList }) {
const [value, setValue] = useState(() => {
const found = myList.find(x => x.name === 'TEST');
return found ? 'Hello World' : null;
});
return (
<div>{value}</div>
);
}
No need for any hook at all, actually, you should just write it like this:
function MyComponent({ myList }) {
const found = myList.some(x => x.name == "TEST");
const value = found ? 'Hello World' : '';
return (
<div>{value}</div>
)
}
Using useEffect or even only useState is conceptually wrong. The useState hook is used when a component needs to have its own independent internal state which isn't the case here, as the value is purely derived from the props. The useEffect hook is an escape hatch typically used when you need to sync with some external imperative API, not at all the case here.
The solution with useMemo is perfectly fine, and more efficient if your filtering condition is very computationally expensive, but it is also a lot more complicated to understand and to maintain, so I would suggest going for the simplest solution until the lack of memoization actually becomes a measurable problem.

Pass function to Context API

I'm dealing with a mix of function components and class components. Every time a click happens in the NavBar I want to trigger the function to validate the Form, it has 5 forms, so each time I'm going to have to set a new function inside the context API.
Context.js
import React, { createContext, useContext, useState } from "react";
const NavigationContext = createContext({});
const NavigationProvider = ({ children }) => {
const [valid, setValid] = useState(false);
const [checkForm, setCheckForm] = useState(null);
return (
<NavigationContext.Provider value={{ valid, setValid, checkForm, setCheckForm }}>
{children}
</NavigationContext.Provider>
);
};
const useNavigation = () => {
const context = useContext(NavigationContext);
if (!context) {
throw new Error("useNavigation must be used within a NavigationProvider");
}
return context;
};
export { NavigationProvider, useNavigation, NavigationContext};
Form.js
import React, { Component } from "react";
import { NavigationContext } from "../hooks/context";
class Something extends Component {
static contextType = NavigationContext;
onClickNext = () => {
// This is the funcion I want to set inside the Context API
if(true){
return true
}
return false;
};
render() {
const { setCheckForm } = this.context;
setCheckForm(() => () => console.log("Work FFS"));
return (
<>
<Button
onClick={this.onClickNext}
/>
</>
);
}
}
export default Something;
The problem when setting the function it throws this error:
Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
And setting like setCheckForm(() => console.log("Work FFS"));, it triggers when rendered.
Render method of React.Component runs whenever state changes and setCheckForm updates the state whenever that render happens. This creates an infinite loop, this is the issue you are having there.
So, this is a lifecycle effect, you have to use that function inside componentDidMount if you want to set it when the component first loads.
While this solves your problem, I wouldn't suggest doing something like this. React's mental model is top to bottom, data flows from parent to child. So, in this case, you should know which component you are rendering from the parent component, and if you know which component to render, that means you already know that function which component is going to provide to you. So, while it is possible in your way, I don't think it is a correct and Reactish way to handle it; and it is probably prone to break.

Does this "custom react hook" breaks the hooks law?

I am using many useCallbacks in useEffects in the same format syntax and feel like shortening them. So, I map them in this hook. effects is an array of useCallback functions.
import React from 'react';
const useEffects = (effects: Function[]) =>
effects.map((effect) =>
React.useEffect(() => {
effect();
}, [effect])
);
export default useEffects;
Hooks can't be defined inside an array.
I wrote an article recently about the rendering order of hooks. https://windmaomao.medium.com/understanding-hooks-part-4-hook-c7a8c7185f4e
It really goes down to below snippet how Hooks is defined.
function hook(Hook, ...args) {
let id = notify()
let hook = hooks.get(id)
if(!hook) {
hook = new Hook(id, element, ...args)
hooks.set(id, hook)
}
return hook.update(...args)
}
When a hook is registered, it requires an unique id, as in line notify(). Which is just a plain i++ placed inside the Component where the hook is written inside.
So if you have a fixed physical location of the hook, you have a fixed id. Otherwise the id can be random, and since the render Component function is called every render cycle, a random id is not going to find the right hook in the next render cycle.
That is why if can not be written before the hook statement either. Just try
const Component = () => {
const b = true
if (!b) return null
const [a] = useState(1)
}
You should get similar error.

React Hook "useState" is called in function that is neither a React function | Typescript

I'm having the problem with calling useState hook in my component.
Everything is working fine. I can define props on Container as well as on Continer.Element.
But when I'm trying to call Hooks inside Container.Element - I'm getting an error.
const Container: React.FC<Props> & {Element: React.FC<ElementProps>} = () => {
return <Container.Element />
}
Container.Element = () => {
const [state, setState] = useState();
return <div>Some JSX code</div>
}
In your code, Container is a valid React component but not the Container.Element.
When you do Container.Element = () => {};: You are just declaring a js function that return some jsx called Element.
To use react hooks, you have to follow the rules of hooks :D
From the react docs :
Only Call Hooks at the Top Level
Don’t call Hooks inside loops, conditions, or nested functions.
Instead, always use Hooks at the top level of your React function.
By following this rule, you ensure that Hooks are called
in the same order each time a component renders.
That’s what allows React to correctly preserve the state
of Hooks between multiple useState and useEffect calls.
(If you’re curious, we’ll explain this in depth below.)
Only Call Hooks from React Functions
Don’t call Hooks from regular JavaScript functions. Instead, you can:
✅ Call Hooks from React function components.
✅ Call Hooks from custom Hooks (we’ll learn about them on the next page).
By following this rule, you ensure that all stateful logic in a component is clearly visible from its source code.
If you need to use hook in your example, you will have to use it in the Container component.
const Container: React.FC<Props> & {Element: React.FC<ElementProps>} = () => {
const [state, setState] = useState();
return <Container.Element />
}
Container.Element = () => {
return <div>Some JSX code</div>
}

More advanced comparison in React's useEffect

I am looking for a way to perform more advanced comparison instead of the second parameter of the useEffect React hook.
Specifically, I am looking for something more like this:
useEffect(
() => doSomething(),
[myInstance],
(prev, curr) => { /* compare prev[0].value with curr[0].value */ }
);
Is there anything I missed from the React docs about this or is there any way of implementing such a hook on top of what already exists, please?
If there is a way to implement this, this is how it would work: the second parameter is an array of dependencies, just like the useEffect hook coming from React, and the third is a callback with two parameters: the array of dependencies at the previous render, and the array of dependencies at the current render.
you could use React.memo function:
const areEqual = (prevProps, nextProps) => {
return (prevProps.title === nextProps.title)
};
export default React.memo(Component, areEqual);
or use custom hooks for that:
How to compare oldValues and newValues on React Hooks useEffect?
In class based components was easy to perform a deep comparison. componentDidUpdate provides a snapshot of previous props and previous state
componentDidUpdate(prevProps, prevState, snapshot){
if(prevProps.foo !== props.foo){ /* ... */ }
}
The problem is useEffect it's not exactly like componentDidUpdate. Consider the following
useEffect(() =>{
/* action() */
},[props])
The only thing you can assert about the current props when action() gets called is that it changed (shallow comparison asserts to false). You cannot have a snapshot of prevProps cause hooks are like regular functions, there aren't part of a lifecycle (and don't have an instance) which ensures synchronicity (and inject arguments). Actually the only thing ensuring hooks value integrity is the order of execution.
Alternatives to usePrevious
Before updating check if the values are equal
const Component = props =>{
const [foo, setFoo] = useState('bar')
const updateFoo = val => foo === val ? null : setFoo(val)
}
This can be useful in some situations when you need to ensure an effect to run only once(not useful in your use case).
useMemo:
If you want to perform comparison to prevent unecessary render calls (shoudComponentUpdate), then useMemo is the way to go
export React.useMemo(Component, (prev, next) => true)
But when you need to get access to the previous value inside an already running effect there is no alternatives left. Cause if you already are inside useEffect it means that the dependency it's already updated (current render).
Why usePrevious works
useRef isn't just for refs, it's a very straightforward way to mutate values without triggering a re render. So the cycle is the following
Component gets mounted
usePrevious stores the inital value inside current
props changes triggering a re render inside Component
useEffect is called
usePrevious is called
Notice that the usePrevious is always called after the useEffect (remember, order matters!). So everytime you are inside an useEffect the current value of useRef will always be one render behind.
const usePrevious = value =>{
const ref = useRef()
useEffect(() => ref.current = value,[value])
}
const Component = props =>{
const { A } = props
useEffect(() =>{
console.log('runs first')
},[A])
//updates after the effect to store the current value (which will be the previous on next render
const previous = usePrevious(props)
}
I hit the same problem recently and a solution that worked for me is to create a custom useStateWithCustomComparator hook.
In your the case of your example that would mean to replace
const [myInstance, setMyInstance] = useState(..)
with
const [myInstance, setMyInstance] = useStateWithCustomComparator(..)
The code for my custom hook in Typescript looks like that:
const useStateWithCustomComparator = <T>(initialState: T, customEqualsComparator: (obj1: T, obj2: T) => boolean) => {
const [state, setState] = useState(initialState);
const changeStateIfNotEqual = (newState: any) => {
if (!customEqualsComparator(state, newState)) {
setState(newState);
}
};
return [state, changeStateIfNotEqual] as const;
};

Resources