functional component rerender on state change - reactjs

I've implemented a window resize event on initial component load. The event detects the window inner width and saves the value in a hook. Based on the width hook there is a second useEffect function, triggered on width change:
export const AppRouter = (props) => {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
useEffect(() => {
setAppLayoutForViewportWidth();
}, [width]);
}
Now to the unexpected behavior: the entire component rerenders on width hook change and not only the useEffect based on the width hook.
Can someone name the reason, why the entire component rerenders? Can I only rerender the width-based useEffect?

Why?
setState({}) always forces to re-render. (unless you return false in: shouldComponentUpdate(nextProps, nextState)) You can check this by putting a console log in
componentDidUpdate(prevProps, prevState) {
console.log("Component did update")
}
Your setWidth(window.innerWidth);will change state due to it being: useState(window.innerWidth); which will force a rerender.
How to prevent it:
If you want to control when it rerenders, create a logic in shouldComponentUpdate to return false when you want to prevent rerender.
If you are in a functional component have a look at React.Memo. More on that in this thread: How can I prevent my functional component from re-rendering with React memo or React hooks?
React.Memo functions similarly to pure component. However, you can
also adjust its behavior by passing it a function which defines what
counts as equal. Basically, this function is shouldComponentUpdate,
except you return true if you want it to not render.
const areEqual = (prevProps, nextProps) => true;
const MyComponent = React.memo(props => {
return /*whatever jsx you like */
}, areEqual);

Related

Callback in props of React component - correct way to fire

Suppose I have a component which is like
function Child(props: { onSelect: () => void }) {
...
useEffect(() => {
// want to fire onSelect here
}, [...]);
...
}
Since props.onSelect might change every render (e.g. arrow function), I can't add it to the dependency list of useEffect and call it directly. I used a reducer instead:
const [, dispatch] = useReducer((state: undefined, action: T) => {
props.onSelect(action);
return undefined;
}, undefined);
useEffect(() => {
dispatch(...);
}, [...]);
But now I get the error "Warning: Cannot update a component (Parent) while rendering a different component (Child)."
What's the correct way to fire the parent's onSelect inside some useEffect?
You mention
Since props.onSelect might change every render (e.g. arrow function), I can't add it to the dependency list of useEffect and call it directly
You can, but you should make sure that it does not change if there is no reason.
You should use a useCallback for it on the parent component, so that it remains the same.
function Parent (){
...
const onSelect = useCallback(() => {
// set local state here
}, []);
...
return ... <Child onSelect={onSelect} />
}

useEffect does not run in initial render

In Piano component I am getting window's dimensions using a hook (useWindowDimensions()). In useEffect, I want to render component everytime width of window changes.
It re-renders the component everytime width changes (because keyData depends on it and it is in dependencyList) but does not render in initial render. So component does not render until I resize the window.
Following code is Piano component.
const svgRef = React.useRef(null);
const svgEl = d3.select(svgRef.current);
const { width } = useWindowDimensions();
const height = width / 4;
const keyData = generateKeyData(88, width);
React.useEffect(() => {
svgEl.selectAll("*").remove();
generatePianoNode(svgEl, keyData);
}, [svgEl, keyData]);
return (
<svg ref={ svgRef } width={ width } height={ height } />
);
I tried to put another useEffect hook with empty dependency list with the same code but it also didn't work.
The useEffect hook actually renders on initial component mount but the problem here is that the way you generate piano node .
You need to somehow put the value in a state and update state so the component re-renders everytime the state changes and also when the component mounts .
const [value , setValue ] = React.useState("1")
React.useEffect(() => {
// your logic
// setValue("2")
}, [value]);
Now the useEffect hook will run on component mount and then everytime the state value changes .
Note
If the value change simultaneously it will cause rendering problem in the component so a better option in this case is assigning an event listener in useEffect like this :
const handleScroll = () => {
// your logic
}
React.useEffect(() => {
if (typeof window !== "undefined"){
window.addEventListener("scroll" , handleScroll)
}
return () => {
// on component unmount remove event listener
if (typeof window !== "undefined"){
window.removeEventListener("scroll" , handleScroll)
}
}
}, []);

useEffect of children component called before useEffect of parent

I am trying to understand why the useEffect of a children component gets called before the Parent component useEffect.
From my understanding, useEffect shoulde be called in the order they are defined based on React's documentation:
React will apply every effect used by the component, in the order they were specified.
This would mean, a Parent's useEffect should be called before a Children's useEffect, but this is not the case.
Example:
const MainComponent = () => {
return {
<ParentComponent />
}
const ParentComponent = () => {
useEffect(() => {
console.log('parent');
}, []);
return <div>Parent <ChildrenComponent /></div>;
}
const ChildrenComponent = () => {
useEffect(() => {
console.log('children');
}, []);
return <div>Children</div>;
}
If you check the console, you should see first children and then parent
Live Code: https://codesandbox.io/s/crazy-butterfly-yn046?file=/src/App.js
My gut tells me this has to do with how react does Layout and Paint of the Parent-Children components?
This:
React will apply every effect used by the component, in the order they were specified.
Would be more precisely stated as:
React will apply every effect used by the component, in the order they were specified in that component.
For example:
const SomeComponent = () => {
useEffect(() => {
console.log('This will run first');
});
useEffect(() => {
console.log('This will run second');
});
// ...
is guaranteed to run in order.
It's not saying anything about the order that effects in different components run.

How to rerender component in useEffect Hook

Ok so:
useEffect(() => {
}, [props.lang]);
What should I do inside useEffect to rerender component every time with props.lang change?
Think of your useEffect as a mix of componentDidMount, componentDidUpdate, and componentWillUnmount, as stated in the React documentation.
To behave like componentDidMount, you would need to set your useEffect like this:
useEffect(() => console.log('mounted'), []);
The first argument is a callback that will be fired based on the second argument, which is an array of values. If any of the values in that second argument change, the callback function you defined inside your useEffect will be fired.
In the example I'm showing, however, I'm passing an empty array as my second argument, and that will never be changed, so the callback function will be called once when the component mounts.
That kind of summarizes useEffect. If instead of an empty value, you have an argument, like in your case:
useEffect(() => {
}, [props.lang]);
That means that every time props.lang changes, your callback function will be called. The useEffect will not rerender your component really, unless you're managing some state inside that callback function that could fire a re-render.
UPDATE:
If you want to fire a re-render, your render function needs to have a state that you are updating in your useEffect.
For example, in here, the render function starts by showing English as the default language and in my use effect I change that language after 3 seconds, so the render is re-rendered and starts showing "spanish".
function App() {
const [lang, setLang] = useState("english");
useEffect(() => {
setTimeout(() => {
setLang("spanish");
}, 3000);
}, []);
return (
<div className="App">
<h1>Lang:</h1>
<p>{lang}</p>
</div>
);
}
Full code:
Simplest way
Add a dummy state you can toggle to always initiate a re-render.
const [rerender, setRerender] = useState(false);
useEffect(()=>{
...
setRerender(!rerender);
}, []);
And this will ensure a re-render, since components always re-render on state change.
You can call setRerender(!rerender) anywhere anytime to initiate re-render.
const [state, set] = useState(0);
useEffect(() => {
fn();
},[state])
function fn() {
setTimeout((), {
set(prev => prev + 1)
}, 3000)
}
The code above will re-render the fn function once every 3 seconds.

How to specify a constructor with a functional component (fat arrow syntax)?

Given this component:
import React from 'react'
import ReactDOM from 'react-dom'
import PropTypes from 'prop-types'
const NewGoalInput = props => {
return (
<input type="text" onKeyUp={handleKeyUp}/>
)
}
const handleKeyUp = (e) => {
if (e.key === "Enter") {
// TODO Add goal
}
}
export default NewGoalInput
How do I add a constructor where I can define the state without using the extends React.Component syntax?
Since it's a stateless component it doesn't have the component lifecycle.
Therefor you can't specify a constructor.
You have to extend React.Component to create a stateful component which then will need a constructor and you'll be able to use the state.
Update
Since React 16.8.0 and Hooks got introduced there are more options.
Hooks are a new feature proposal that lets you use state and other React > features without writing a class. They are released in React as a part of > v16.8.0
Stateless:
import React from "react"
const Stateless = ({name}) => (
<div>{`Hi ${name}`}</div>
);
Stateful:
Has access to component lifecycle methods and local state.
class Stateful extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
componentDidMount() {
const { count } = this.state;
document.title = `You've clicked ${count} times.`;
}
componentDidUpdate() {
const { count } = this.state;
document.title = `You've clicked ${count} times.`;
}
render() {
const { count } = this.state;
return (
<div>
<p>You've clicked {count} times.</p>
<button onClick={() => this.setState({ count: count + 1 })}>
Click me
</button>
</div>
);
}
}
Using Hooks:
Able to use State Hook and Effect Hook.
If you’re familiar with React class lifecycle methods, you can think of useEffect Hook as componentDidMount, componentDidUpdate, and componentWillUnmount combined.
import React, { useState, useEffect } from "react";
const UsingHooks = () => {
const [count, setCount] = useState(0);
// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
// Update the document title using the browser API
document.title = `You've clicked ${count} times.`;
});
return (
// <> is a short syntax for <React.Fragment> and can be used instead of a wrapping div
<>
<p>You've clicked {count} times.</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</>
);
}
Now that we have useState and hooks the answers are kind of out of date. I came across this question because I was doing something wrong. Here's some simplified code of what I was doing.
// set an initial state
const [ value, setValue ] = useState(0)
// gets called after component is re-rendered
useEffect(() => {
// callback to parent that set props
props.update()
})
// if we have an existing value passed in
if (props.value) {
setValue(props.value)
}
This code was converted from a stateful class to a function using hooks, originally setting the default props in the constructor - but functions don't have constructors and that check happens every time the component re-renders:
calls useState
triggers re-render
useEffect is triggerd
parent is called which sets the props
props update so child renders again
GOTO 1
As you can see this results in an infinite loop. The solution is really quite simple. Here's a mock diff from the original.
- const [ value, setValue ] = useState(0)
+ const [ value, setValue ] = useState(props.value || 0)
- if (props.value) {
- setValue(props.value)
- }
Basically, just initialise the state from the props and don't do silly things like calling useState except in response to an event or callback of some type.
You can use useMemo hook (as below) to demonstrate as constructor for functional component. Somebody suggested to use useEffect but it will be invoked after render.
useMemo(() => {
console.log('This is useMemo')
}, []);
you could set a useState as the first line inside of your functional component and add a function as "initial value":
const MyComponentName = props => {
useState(() => {
console.log('this will run the first time the component renders!');
});
return <div>my component!</div>;
};
You don't. The kind of component in your example is called "stateless functional component". It has no state and no lifecycle methods. If you want your component to be stateful you'll have to write it as a class component.
To simulate constructor in FC use useEffect.
useEffect(() => {
... here your init code
}, []);
That's it! EZ! This useEffect runs only once when the component loads and never runs after, just don't forget to add square brackets at the end.
For those who want to run a function once before the component is mounted, here is a hook (written in TypeScript).
Normally useEffect and useLayoutEffect suffice, but they run after the component is mounted, and sometimes you want to run code before that happens (like a constructor).
import React, { useRef } from "react";
function useOnce<Type>(callBack: () => Type): Type {
const result = useRef<Type | null>(null);
if (result.current !== null) {
return result.current;
}
result.current = callBack();
return result.current;
}
const Component: React.FC<{}> = () => {
const result = useOnce(() => {/* Code you would normally put in a constructor */});
return <div />
}
Alternatively, you can use react-afc
import { afc, reactive } from 'react-afc'
function heavyCalc() {/*...*/}
const Conponent = afc(props => {
// Called once, before the first render
const state = reactive({
name: 'Stack',
inputsCount: 0
})
// Without useMemo(..., [])
const result = heavyCalc()
// The function is created once and does not cause
// a re-render of child components
function onInput(e) {
state.inputsCount++
state.name = e.currentTarget.value
}
// Saved between renders (no longer need useRef)
let rendersCount = 0
// Must return the render-function
return () => {
// The function works like a regular react-component
// Here you can use the usual hooks
rendersCount++
return (
<input onChange={onInput} value={state.name}/>
)
}
})
The package has the necessary methods for working with state (including redux), react-hooks, lifecycle methods and context

Resources