useMemo behaves different for inline component vs. normal component - reactjs

I have the following React code
const { useState, useMemo, Fragment } = React;
function Rand() {
return <Fragment>{Math.random()}</Fragment>;
}
const App = () => {
const [show, setShow] = useState(true);
// The inline component gets memoized. But <Rand /> does not
const working = useMemo(() => <Fragment>{Math.random()}</Fragment>, []);
// The rand component is not memoized and gets rerendred
const notWorking = useMemo(() => <Rand />, []);
return(
<Fragment>
<button
onClick={() => {
setShow(!show);
}}>
{show?"Hide":"Show"}
</button>
<br />
Working:
{show && working}
<br />
Not Working:
{show && notWorking}
</Fragment>
);
}
ReactDOM.render(
<App />,
document.getElementById("root")
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
It uses useMemo 2 times.
The first time it uses an inline component to "initialize" and memoize a component ( const working = useMemo(() => <>{Math.random()}</>, []);)
The second time it uses a component which was made outside the app component (const notWorking = useMemo(() => <Rand />, []);)
Both components used in the useMemo function have the exact same code, which is <>{Math.random()}</>.
Here comes the unexpected part, when I hide (Click the button) and show the two memoized components again, they behave differently. The first one will always show the same random number which it got when it first got initialzied. While the seconds one will re-initialize each time.
First render
Second render (hide)
Third render (show again)
You can see from the screenshots that the first component's random number stays the same, while the second one does not.
My Questions:
How can I prevent in both cases to re-render/re-initialize the component?
Why does it currently behave as it does?
Interestingly, it does get memoized if I use a counter instead of show/hide:
const { useState, useMemo, Fragment } = React;
function Rand() {
return <Fragment>{Math.random()}</Fragment>;
}
const App = () => {
const [counter, setCounter] = useState(0);
// The inline component gets memoized. But <Rand /> does not
const working = useMemo(() => <Fragment>{Math.random()}</Fragment>, []);
// The rand component is not memoized and gets rerendred
const notWorking = useMemo(() => <Rand />, []);
return(
<Fragment>
<button
onClick={() => {
setCounter(c => c + 1);
}}>
Update ({counter})
</button>
<br />
Working:
{working}
<br />
Not Working:
{notWorking}
<br />
<code>Rand</code> used directly:
<Rand />
</Fragment>
);
}
ReactDOM.render(
<App />,
document.getElementById("root")
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
Here is a codepen to try it yourself https://codepen.io/brandiatmuhkuh/pen/eYWmyWz

Why does it currently behave as it does?
<Rand /> doesn't call your component function. It just calls React.createElement to create the React element (not an instance of it). Your component function is used to render an element instance, if and when you use it. In your "working" example you're doing:
<>{Math.random()}</>
...which calls Math.random and uses its result as text (not a component) within the fragment.
But your "not working" example just does:
<Rand />
The element is created, but not used, and your function isn't called. The "your function isn't called" part may be surprising — it was to me when I started using React — but it's true:
const { Fragment } = React;
function Rand() {
console.log("Rand called");
return <Fragment>{Math.random()}</Fragment>;
}
console.log("before");
const element = <Rand />;
console.log("after");
// Wait a moment before *using* it
setTimeout(() => {
ReactDOM.render(
element,
document.getElementById("root")
);
}, 1000);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
How can I prevent in both cases to re-render/re-initialize the component?
If you do what you've done in your example, which is to take the mounted component out of the tree entirely, you're unmounting the component instance; when you put it back, your function will get called again, so you'll get a new value. (This is also why the version with the counter doesn't exhibit this behavior: the component instance remained mounted.)
If you want to memoize what it shows, one approach is to pass that to it as a prop, and memoize what you pass it:
const { useState, useMemo, Fragment } = React;
function Rand({text}) {
return <Fragment>{text}</Fragment>;
}
const App = () => {
const [show, setShow] = useState(true);
const working = useMemo(() => <Fragment>{Math.random()}</Fragment>, []);
const randText = useMemo(() => String(Math.random()), []);
return(
<Fragment>
<button
onClick={() => {
setShow(!show);
}}>
{show?"Hide":"Show"}
</button>
<br />
Working:
{show && working}
<br />
Also Working Now:
{show && <Rand text={randText} />}
</Fragment>
);
}
ReactDOM.render(
<App />,
document.getElementById("root")
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>

Related

react useState not re rendering

I have a pretty simple useEffect hook
const [tagsWithData, setTagsWithData] = useState([]);
useEffect(() => {
....
const finalsTags = temp.map((item) => item.name);
setTagsWithData(finalsTags);
}, []);
Inside of return, I have condition to render the input tag
{tagsWithData.length !== 0 ? (
<TagsInput
selectedTags={selectedTags}
tags={tagsWithData}
/>
) : (
<TagsInput
selectedTags={selectedTags}
tags={tags}
/>
)}
The above code always stays on 0 and it does not move to the else condition.
What am I making wrong here.
Thank you
Your useEffect is not being told to update. useEffect needs to be passed the value/dependencies that it needs to (trigger the) update on. Without it, the effect will only run once on (initial) component render
const [tagsWithData, setTagsWithData] = useState([]);
useEffect(() => {
....
const finalsTags = temp.map((item) => item.name);
setTagsWithData(finalsTags);
}, [temp]); // <--- add this
Below is a small example illustrating the differences. Click on the button, and check out the output of both effectWithDep and effectWithoutDep. You'll notice only effectWithDep will update.
// Get a hook function
const { useState, useEffect } = React;
const Example = ({title}) => {
const [count, setCount] = useState(0);
const [effectWithDep, setEffectWithDep] = useState(0);
const [effectWithoutDep, setEffectWithoutDep] = useState(0);
useEffect(() => {
setEffectWithDep(count)
}, [count])
useEffect(() => {
setEffectWithoutDep(count)
}, [])
return (
<div>
<p>{title}</p>
<p>effectWithDep: {effectWithDep}</p>
<p>effectWithoutDep: {effectWithoutDep}</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
};
// Render it
ReactDOM.render(
<Example title="Example using Hooks:" />,
document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>

Context rerender prevention with useMemo()

When researching contexts I've stumbled across a common pattern more than once:
const storeContext = React.createContext()
const Provider = (children) => {
const [state, setState] = React.useState();
const ctxValue = React.useMemo(() => [state, setState], [state]);
return (
<storeContext.Provider value={ctxValue}>
{children}
</storeContext.Provider>
)
}
Many people seem to use useMemoin this case, and I'm not sure why. At first glance, it seems to make sense since passing an object/array directly would mean that this object would get recreated on every render and trigger the children to be rerendered since a new reference is passed. At second glance, I struggle with the pattern.
I'm not sure how this pattern could potentially prevent any rerenders. If a component up the tree changes, all children (including the context) rerender anyway, and if state changes, the provider rerenders (rightfully) itself as well as all children.
The only thing that I see useMemo do is to save a tiny bit of memory since we're not recreating the ctxValue object on every render.
I'm not sure if I'm missing something here, and would love some input.
The reason is because every render would create a new Array instance. By using useMemo, the array passed to the context is only ever changed when state changes. A better way to do this would be to just pass the result of useState to the context:
const storeContext = React.createContext()
const Provider = (children) => {
const ctxValue = React.useState();
return (
<storeContext.Provider value={ctxValue}>
{children}
</storeContext.Provider>
)
}
Update
So, I was pretty curious as to how React would handle re-renders and contexts updating. I was really surprised to find that React will only re-render a component if the props change.
For example, if you have something like this:
function MyComponent(){
const ctxValue = useContext(MyContext);
return (<div>
value: {ctxValue}
<MyOtherComponent />
</div>);
}
And you render that component, MyOtherComponent will only render once, ever, assuming the component doesn't use any hooks that can update its internal state.
The same goes for Context.Provider. The only time it will re-render is when the value updates, but its children will not re-render unless they directly depend on that context.
I create a simple sandbox to demonstrate this.
Basically, I set up a lot of different ways to nest children, and some children rely on the context, and others don't. If you click the "+" button, the context is updated and you'll see the render count only increase for those components.
const Ctx = React.createContext();
function useRenderCount(){
const count = React.useRef(0);
count.current += 1;
return count.current;
}
function wrapInRenderCount(name,child){
const count = useRenderCount();
return (
<div>
<div>
{name} was rendered {count} times.
</div>
<div style={{ marginLeft: "1em" }}>{child}</div>
</div>
);
}
function ContextProvider(props) {
const [count, setState] = React.useState(0);
const increase = React.useCallback(() => {
setState((v) => v + 1);
}, []);
const context = React.useMemo(() => [increase, count], [increase, count]);
return wrapInRenderCount('ContextProvider',
<Ctx.Provider value={context}>
{props.children}
</Ctx.Provider>
);
}
function ContextUser(props){
const [increase, count] = React.useContext(Ctx);
return wrapInRenderCount('ContextUser',
<div>
<div>
Count: {count}
<button onClick={increase}>
++
</button>
</div>
<div>
{props.children}
</div>
</div>
);
}
function RandomWrapper(props){
return wrapInRenderCount('RandomWrapper',<div>
<div>
Wrapped
</div>
<div style={{marginLeft:'1em'}}>
{props.children}
</div>
</div>);
}
function RandomContextUserWrapper(props){
return wrapInRenderCount('RandomContextUserWrapper',
<div>
<ContextUser></ContextUser>
{props.children}
</div>
)
}
function App() {
return wrapInRenderCount('App',
<RandomWrapper>
<ContextProvider>
<RandomWrapper>
<ContextUser>
<RandomWrapper>
<RandomContextUserWrapper>
<RandomWrapper>
<ContextUser/>
</RandomWrapper>
</RandomContextUserWrapper>
</RandomWrapper>
</ContextUser>
<RandomWrapper>
<RandomContextUserWrapper />
</RandomWrapper>
</RandomWrapper>
</ContextProvider>
</RandomWrapper>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(
<App />,
rootElement
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="root"></div>

How to check if long list got rendered in React

Im trying to display a very long list from .json file (2k+ nodes with multiple lines of text). Is there a way to set useState variable after list finishes rendering itself cause useEffect refused to work
import React from 'react';
import LongList from './LongList.json';
const LongList = () => {
const [isLoaded,setIsLoaded] = React.useState(false);
React.useEffect(() => {
setIsLoaded(true);
}, [setIsLoaded]);
return (
<div>
{LongList.map(element => (
<div key={element.text}>{element.text}</div>
))}
</div>
);
};
You can do something like that by checking the index of the current item:
{LongList.map((element, index) => (
<div key={element.text}>{element.text}</div>
if(index === LongList.length - 1) {
// it is loaded
}
))}
You're on the right track with useEffect. I believe part of the issue you're having is due to using setIsLoaded as the second argument to useEffect. Instead, use [], which tells React that your effect doesn’t depend on any values from props or state, so it never needs to re-run. More info in the React docs.
Here's an example, with a console log in the useEffect callback showing it's only run once.
const data = Array.from(Array(10001).keys());
const LongList = ({data}) => {
const containerRef = React.useRef(null);
const [height, setHeight] = React.useState(0);
React.useEffect(() => {
console.log('Height: ', containerRef.current.clientHeight);
setHeight(containerRef.current.clientHeight);
}, []);
return (
<div>
<div>Height: {height}</div>
<div ref={containerRef}>
{data.map(element => (
<div key={element}>{element}</div>
))}
</div>
</div>
);
};
ReactDOM.render(<LongList data={data} />, document.getElementById('app'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
<div id="app"></div>

Input is loosing focus on hooks update

I'm new to reactjs and I'm trying read data from input. Problem is when I type a sign, my input loose focus. But only when all logic is inside function.
When Input with button and logic is in different file - it's working. I don't really know why...
I have created separate file with same code and import it to sollution - it's ok.
I have tried with onChange={handleChange} - lost focus as well.
export default function MyInput(){
const [citySearch, updateCitySearch] = useState();
function searchCityClick(){
alert(citySearch);
}
const SearchComponent = ()=> (
<div>
<input
value={citySearch}
onChange={(e) => updateCitySearch(e.target.value)}/>
<Button variant="contained" color="primary" onClick={searchCityClick}>
Search
</Button>
</div>
);
return(
<div>
<div>
<SearchComponent />
</div>
</div>
)}
The SearchComponent is a functional component, and shouldn't be defined inside another component. Defining SearchComponent inside MyInput will cause SearchComponent to be recreated (not rerendered), and in essence it's DOM would be removed, and then added back on every click.
The solution is pretty straightforward, extract SearchComponent from MyInput, and pass the functions, and the data via the props object:
const { useState, useCallback } = React;
const SearchComponent = ({ citySearch, updateCitySearch, searchCityClick }) => (
<div>
<input
value={citySearch}
onChange={e => updateCitySearch(e.target.value)} />
<button onClick={searchCityClick}>Search</button>
</div>
);
const MyInput = () => {
const [citySearch, updateCitySearch] = useState('');
const searchCityClick = () => alert(citySearch);
return(
<div>
<SearchComponent
citySearch={citySearch}
updateCitySearch={updateCitySearch}
searchCityClick={searchCityClick} />
</div>
);
};
ReactDOM.render(
<MyInput />,
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>
I am also new to React, so take my explanation with a pinch of salt (hopefully someone else can elaborate).. I believe it has something to do with nesting components and how React is re-rendering..
If you use SearchComponent as a variable, instead of an anonymous function, this works as expected.
I am also curious as to why using nested functions like that (when using JSX) causes this behavior... possibly an anti-pattern?
function MyInput() {
const [citySearch, updateCitySearch] = React.useState();
function searchCityClick() {
alert(citySearch);
}
const SearchComponent = (
<div>
<input
value={citySearch}
onChange={(e) => updateCitySearch(e.target.value)}/>
<button variant="contained" color="primary" onClick={searchCityClick}>
Search
</button>
</div>
);
return (
<div>
<div>
{SearchComponent}
</div>
</div>
);
}
let div = document.createElement("div");
div.setAttribute("id", "app");
document.body.append(div);
ReactDOM.render(<MyInput />, document.getElementById("app"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>
Even if you change the nested function to an "actual component", focus is lost after each key press (aka onChange)..
Does not work:
function MyInput() {
const [citySearch, updateCitySearch] = React.useState();
function searchCityClick() {
alert(citySearch);
}
const SearchComponent = () => {
return (
<div>
<input
value={citySearch}
onChange={(e) => updateCitySearch(e.target.value)}/>
<button variant="contained" color="primary" onClick={searchCityClick}>
Search
</button>
</div>
);
}
return (
<div>
<div>
<SearchComponent />
</div>
</div>
);
}
This happens because the useState hook is not "hooked" to your SearchComponent, but your MyInput component. Whenever, you call updateCitySearch() you change the state of MyInput, thus forcing the entire component to re-render.
SearchComponent, is explicitly defined inside MyInput. When citySearch-state is updated, SearchComponent loses focus because the initial virual DOM surrounding it is no longer intact, instead you have a completely new piece of DOM. Essentially, you are creating a brand new SearchComponent each time the MyInput is updated by state.
Consider the following example:
function App() {
const [citySearch, updateCitySearch] = useState("");
console.log("rendered App again"); //always prints
const SearchComponent = () => {
console.log("rendered Search"); //always prints
const searchCityClick = () => {
alert(citySearch);
};
return (
<div>
<input
value={citySearch}
onChange={e => {
updateCitySearch(e.target.value);
}}
/>
<button onClick={searchCityClick}>Search</button>
</div>
);
};
return (
<div>
<div>
<SearchComponent />
</div>
</div>
);
}
Every time you update state, you would trigger both console.log(), the App component re-renders and SearchComponent gets re-created. A new iteration of myInput is rendered each time and a new SearchComponent gets created.
But if you were to define useState inside SearchComponent, then only SearchComponent will re-render whens state changes, thus leaving the original myInput component unchanged, and the current SearchComponent intact.
function App() {
console.log("rendered App again"); //would never print a 2nd time.
const SearchComponent = () => {
const [citySearch, updateCitySearch] = useState("");
console.log("rendered Search"); //would print with new state change
const searchCityClick = () => {
alert(citySearch);
};
return (
<div>
<input
value={citySearch}
onChange={e => {
updateCitySearch(e.target.value);
}}
/>
<button onClick={searchCityClick}>Search</button>
</div>
);
};
return (
<div>
<div>
<SearchComponent />
</div>
</div>
);
}

React - make input appear with text selected

I have a functional component, that uses hooks.
const [editMode, setEditMode] = useState(false);
...
return (
...
{editMode && <input value="Some value">}
}
When editMode is changed to true - the input field is appearing and I want it to appear with already selected text inside of it. How can I do this?
You can use the useRef hook to create a ref and put it on your input element, and use the useEffect hook to run a function every time editMode changes. If editMode is true, you can invoke the select method on the ref.current element.
Example
const { useState, useRef, useEffect } = React;
function App() {
const [value, setValue] = useState("Some value");
const [editMode, setEditMode] = useState(false);
const ref = useRef();
useEffect(() => {
if (editMode) {
ref.current.select();
}
}, [editMode]);
return (
<div>
<button onClick={() => setEditMode(!editMode)}>Toggle edit</button>
<div>
{editMode && (
<input
ref={ref}
value={value}
onChange={e => setValue(e.target.value)}
/>
)}
</div>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root"></div>

Resources