React idiomatic controlled input (useCallback, props and scope) - reactjs

I was building a good old read-fetch-suggest lookup bar when I found out that my input lost focus on each keypress.
I learnt that because my input component was defined inside the header component enclosing it, changes to the state variable triggered a re-render of the parent which in turn redefined the input component, which caused that behaviour. useCallback was to be used to avoid this.
Now that I did, the state value remains an empty string, even though it's callback gets called (as I see the keystrokes with console.log)
This gets fixed by passing the state and state setter to the input component as props. But I don't quite understand why. I'm guessing that the state and setter which get enclosed in the useCallback get "disconnected" from the ones yield by subsequent calls.
I would thankfully read an explanation clearing this out. Why does it work one way and not the other? How is enclosing scope trated when using useCallback?
Here's the code.
export const Header = () => {
const [theQuery, setTheQuery] = useState("");
const [results, setResults] = useState<ResultType>();
// Query
const runQuery = async () => {
const r = await fetch(`/something`);
if (r.ok) {
setResults(await r.json());
}
}
const debouncedQuery = debounce(runQuery, 500);
useEffect(() => {
if (theQuery.length > 3) {
debouncedQuery()
}
}, [theQuery]);
const SearchResults = ({ results }: { results: ResultType }) => (
<div id="results">{results.map(r => (
<>
<h4><a href={`/linkto/${r.id}`}>{r.title}</a></h4>
{r.matches.map(text => (
<p>{text}</p>
))}
</>
))}</div>
)
// HERE
// Why does this work when state and setter go as
// props (commented out) but not when they're in scope?
const Lookup = useCallback((/* {theQuery, setTheQuery} : any */) => {
return (
<div id='lookup_area'>
<input id="theQuery" value={theQuery}
placeholder={'Search...'}
onChange={(e) => {
setTheQuery(e.target.value);
console.log(theQuery);
}}
type="text" />
</div>
)
}, [])
return (
<>
<header className={`${results ? 'has_results' : ''}`}>
<Lookup /* theQuery={theQuery} setTheQuery={setTheQuery} */ />
</header>
{results && <SearchResults results={results} />}
</>
)
}

It's generally not a good idea to have what are essentially component definitions inside of another component's render function as you get all the challenges you have expressed. But I'll come back to this and answer your original question.
When you do the following:
const Lookup = useCallback((/* {theQuery, setTheQuery} : any */) => {
return (
<div id='lookup_area'>
<input id="theQuery" value={theQuery}
placeholder={'Search...'}
onChange={(e) => {
setTheQuery(e.target.value);
console.log(theQuery);
}}
type="text" />
</div>
)
}, [])
You are basically saying that when the Header component mounts, store a function that returns the JSX inside of Lookup, and also put this in a cache on mount of the component and never refresh it on subsequent renders. For subsequent renders, react will pull out the definition from that initial render rather than redefining the function -- this is called memorization. It means Lookup will be referentially stable between all renders.
What defines that it's only on the mount is the deps array []. The deps array is a list of things, that when changed between renders, will trigger the callback to be redefined, which at the same time will pull in a new scope of the component its enclosed within from that current render. Since you have not listed everything used inside of the callback function from the parent scope inside of the deps array, you are actually working around a bug that would be flagged if you used the proper lifting rules. It's a bug because if something used inside like theQuery and setTheQuery changes, then the Lookup callback will not refresh/be redefined -- it will use the one stored in the local cache on mount which is in turn referencing stale copies of those values. This is a very common source of bugs.
That said since you've done this, Lookup remains stable and won't be refreshed. As you said, if it does refresh, you are going to see it gets remounted and things like its implicit DOM state (focus) are lost. In react, component definitions need to be referentially stable. Your useCallbacks are basically component definitions, but inside of another component render, and so it's being left up to you to deal with the tricky memorization business to "prevent" it from being redefined on each render. I'll come back to how you work around this properly shortly.
When you add theQuery={theQuery} setTheQuery={setTheQuery} you are working around the actual root problem by passing this data through to the callback from the parent scope so that it does not need to use the stale ones it has at hand from the initial render.
But what you have done is essentially write a component inside of another component, and that makes things much less encapsulated and gives rise to the problems you are seeing. You just need to simply define Lookup as its own component. And also, SearchResults.
const Lookup = ({theQuery, setTheQuery}: any)) => {
return (
<div id='lookup_area'>
<input id="theQuery" value={theQuery}
placeholder={'Search...'}
onChange={(e) => {
setTheQuery(e.target.value);
console.log(theQuery);
}}
type="text" />
</div>
)
}
const SearchResults = ({ results }: { results: ResultType }) => (
<div id="results">{results.map(r => (
<>
<h4><a href={`/linkto/${r.id}`}>{r.title}</a></h4>
{r.matches.map(text => (
<p>{text}</p>
))}
</>
))}</div>
)
export const Header = () => {
const [theQuery, setTheQuery] = useState("");
const [results, setResults] = useState<ResultType>();
// Query
const runQuery = async () => {
const r = await fetch(`/something`);
if (r.ok) {
setResults(await r.json());
}
}
const debouncedQuery = debounce(runQuery, 500);
useEffect(() => {
if (theQuery.length > 3) {
debouncedQuery()
}
}, [theQuery]);
return (
<>
<header className={`${results ? 'has_results' : ''}`}>
<Lookup theQuery={theQuery} setTheQuery={setTheQuery} />
</header>
{results && <SearchResults results={results} />}
</>
)
}
Since the components are defined outside of render, they are by definition defined once and they can't access the scope of the Header component directly without you passing it to it via props. This is a correctly encapsulated and parameterized component that removes the opportunity to get into a scoping and memoization mess.
It's not desirable to enclose components in components just so you can access the scope. That is the wrong mindset to move forward with. You should be looking to break up the components as much as makes sense. You can always have multiple components in one file. In React, a component is not just a unit of reuse, but also the primary way of encapsulating logic. It's supposed to be used in this exact situation.

Related

Why doesn't memoization of a React functional component call with useMemo() hook work?

Okay, I think I might as well add here that I know that code doesn't look too good, but I thought it would do the job, so I would be glad if you could explain why not only it would not be a good practice if it worked, but why it doesn't work. I'd be glad to hear your solutions to the problem too!
I'm having a hard time understanding why the chunk of code below doesn't seem to work as intended by which I mean memoizing <First /> and <Second /> functional components' return values and not calling their functions on every <App /> render. I thought that since <SomeComponent /> expression returns an object, it would be possible to simply memoize it and go with. Doesn't seem to work and I can't wrap my head around of as to why.
On a side note, I would also be thankful if you could explain why rendering <App /> component causes the renders.current value increment by two while only calling the console.log once.
Thanks a lot for your help!
import "./styles.css";
import React from "react";
const First = () => {
const renders = React.useRef(0);
renders.current += 1;
console.log('<First /> renders: ', renders.current);
return <h1>First</h1>;
}
const Second = () => {
const renders = React.useRef(0);
renders.current += 1;
console.log('<Second /> renders: ', renders.current);
return <h1>Second</h1>;
}
const App = () => {
const [isSwapped, setIsSwapped] = React.useState(false);
const [, dummyState] = React.useState(false);
const first = React.useMemo(() => <First />, []);
const second = React.useMemo(() => <Second />, []);
const renders = React.useRef(0);
renders.current += 1;
console.log('<App /> renders: ', renders.current);
return (
<div className="App">
<button onClick={() => setIsSwapped((isSwapped) => !isSwapped)}>Swap</button>
<button onClick={() => dummyState((state) => !state)}>Re-render</button>
{isSwapped ? (
<>
{first}
{second}
</>
) : (
<>
{second}
{first}
</>
)}
</div>
);
}
Edit: Thanks for replies, guys, this is the version that does what was intended: https://codesandbox.io/s/why-doesnt-memoization-of-a-react-functional-component-call-with-usememo-hook-forked-gvnn0?file=/src/App.js
For the second part of your question relating to why the value of render.current might be updating twice rather than once it might be because you are using React.StrictMode in your index.jsx file. Removing StrictMode did seem to fix the issue in this example. Also check out this link for more info.
As for the first issue, it seems that on clicking the re-render button neithr of the child components (First or Second) re-render. However, when we swap them not only do they re-render but the components are also unmounted and then re-mounted. You can see this behaviour in the above example, as well. Providing unique keys for both components seems to fix this issue. I'm also a beginner at React and not entirely sure of what's happening behind the scenes, however this is what I have gathered so far based on the documentation: When react finds that a child (say the first child) has a different type compared to what the type was on the previous render (in our case the type switches between First and Second) it unmounts the child node and all of its children, then re-mounts them, then continues with the rendering process. This is supported by the console logs we can see int the above example. By including unqiue keys we let react know that despite components First and Second being in different locations they are the same component after all. So, react doesn't bump into a scenario where a child has swapped types.
I tried your code with the components' return values memoized via the useMemo hook and then wrapped each with React.memo and the result appeared to be the same for me.
Using the memo Higher Order Component to decorate a component will memoize the rendered result.
If your component renders the same result given the same props, you
can wrap it in a call to React.memo for a performance boost in some
cases by memoizing the result. This means that React will skip
rendering the component, and reuse the last rendered result.
const First = React.memo(() => {
const renders = React.useRef(0);
React.useEffect(() => {
renders.current += 1;
console.log("<First /> renders: ", renders.current);
});
return <h1>First</h1>;
});
const Second = React.memo(() => {
const renders = React.useRef(0);
React.useEffect(() => {
renders.current += 1;
console.log("<Second /> renders: ", renders.current);
});
return <h1>Second</h1>;
});
You'll see I've also addressed the unintentional side-effects (console log and ref mutation) in the following question:
On a side note, it would also be cool if you could help me with
understanding why parent component seems to be adding +2 to
renders.current on every render even though if I put a
console.log('render') in the component, it will only show up once.
This is because there are two phases of the component lifecycle, the "render phase" and the "commit phase".
Notice that the "render" method occurs during the "render phase" and recall that the entire component body of functional components is the "render method". This means that React can and will possible render the component several times in order to compute a diff for what needs to be rendered to the DOM during the "commit phase". The "commit phase" is what is traditionally considered rendering a component (to the DOM). Note also that this phase is where side-effect can be run, i.e. useEffect.
Place the logs and ref update in an useEffect hook to run them once per render cycle.
function App() {
const [isSwapped, setIsSwapped] = React.useState(false);
const [, dummyState] = React.useState(false);
const renders = React.useRef(0);
React.useEffect(() => {
renders.current += 1;
console.log("<App /> renders: ", renders.current);
});
return (
<div className="App">
<button onClick={() => setIsSwapped((isSwapped) => !isSwapped)}>
Swap
</button>
<button onClick={() => dummyState((state) => !state)}>Re-render</button>
{isSwapped ? (
<>
<First />
<Second />
</>
) : (
<>
<Second />
<First />
</>
)}
</div>
);
}
Demo

Why isn't my state hook updating correctly?

I've created a minimal cutting of my code to show the issue, seen below.
const PlayArea = (props) => {
const [itemsInPlay, setItemsInPlay] = useState([
{id: 'a'},
{id: 'b'}
]);
const onItemDrop = (droppedItem) => {
setItemsInPlay([...itemsInPlay, droppedItem]);
};
return (
<>
<Dropzone onDrop={onItemDrop} />
<div>
{itemsInPlay.map(item => (
<span
key={item.id}
/>
))}
</div>
</>
);
};
The dropzone detects a drop event and calls onItemDrop. However, for reasons I don't understand, I can only drop in one item. The first item I drop is correctly appended to itemsInPlay and it re-renders correctly with a third span in addition to the starting two.
However, any subsequent item I drop replaces the third item rather than being appended. It's as though onItemDrop had a stored reference to itemsInPlay which was frozen with the initial value. Why would that be? It should be getting updated on re-render with the new value, no?
The Dropzone sets its subscription token only once, when the component is initially rendered. When that occurs, the callback passed to setSubscriptionToken contains a stale value of the onCardDrop prop - it will not automatically update when the component re-renders, since the subscription was added only once.
You could either unsubscribe and resubscribe every time onCardDrop changes, using useEffect, or use the callback form of setItemsInPlay instead:
const onItemDrop = (droppedItem) => {
setItemsInPlay(items => [...items, droppedItem]);
};
This way, even if an old version of onItemDrop gets passed around, the function won't depend on the current binding of itemsInPlay being in the closure.
Another way to solve it would be to change Dropzone so that it subscribes not just once, but every time the onCardDrop changes (and unsubscribing at the end of a render), with useEffect and a dependency array.
Regardless of what you do, it would also be a good idea to unsubscribe from subscriptions when the PlayArea component dismounts, something like:
const [subscriptionToken, setSubscriptionToken] = useState<string | null>(null);
useEffect(
() => {
const callback = (topic: string, dropData: DropEventData) => {
if (wasEventInsideRect(dropData.mouseUpEvent, dropZoneRef.current)) {
onCardDrop(dropData.card);
setDroppedCard(dropData.card);
}
};
setSubscriptionToken(PubSub.subscribe('CARD_DROP', callback));
return () => {
// Here, unsubscribe from the CARD_DROP somehow,
// perhaps using `callback` or the subscription token
};
},
[] // run main function once, on mount. run returned function on unmount.
);

What invokves the 2nd function call when using React Hooks?

I wrote the following React exercise which uses no hooks and renders a button.
const Button = ({ onClick }) => <button onClick={onClick}>Do Nothing</button>;
const Base = () => {
const onClickFunction = (() => {
console.log("Creating OnClick Function");
return () => {};
})();
return (
<div className="App">
<h1>Hello</h1>
<Button onClick={onClickFunction} />
</div>
);
};
onClickFunction uses a self-invoking function, so that I can place a console.log to see the following behaviour. In this example, when Base is rendered, the message Creating OnClick Function appears only once 👍
If I change Base to the following however, adding a hook usage:
const Button = ({ onClick }) => <button onClick={onClick}>Do Nothing</button>;
const Base = () => {
const notUsedRef = React.useRef();
const onClickFunction = (() => {
console.log("Creating OnClick Function");
return () => {};
})();
return (
<div className="App">
<h1>Hello</h1>
<Button onClick={onClickFunction} />
</div>
);
};
You will see the Creating OnClick Function message twice.
This CodeSandbox illustrates what I've been seeing: https://codesandbox.io/s/dawn-forest-99clo?file=/src/App.js
Using React DevTools Profiler, we can see there is no rerender of this component.
Using <React.Profiler, it reports this component also didn't update.
I know that using React.useCallback wouldn't trigger a second invokation, however the question would still stand why we are in the situation Base is called twice.
My question is: why and what is triggering Base to be invoked when there is no need for a rerender.
This is due to the way React implements hooks.
If you invoke any hook, even if you don't use the resulting value, you are telling React to render twice before mounting, even if the props don't change. You can substitute the usage of useRef by useState, useEffect, etc. Try below.
You can also wrap your component with React.memo. Every function defined inside the function is recreated in every render.
https://codesandbox.io/s/elastic-water-y18w0?file=/src/App.js
EDIT: Only happens during development and in components wrapped by React.StrictMode. In the words of gaearon:
It's an intentional feature of the StrictMode. This only happens in
development, and helps find accidental side effects put into the
render phase. We only do this for components with Hooks because those
are more likely to accidentally have side effects in the wrong place.
https://github.com/facebook/react/issues/15074

How to Unmount React Functional Component?

I've built several modals as React functional components. They were shown/hidden via an isModalOpen boolean property in the modal's associated Context. This has worked great.
Now, for various reasons, a colleague needs me to refactor this code and instead control the visibility of the modal at one level higher. Here's some sample code:
import React, { useState } from 'react';
import Button from 'react-bootstrap/Button';
import { UsersProvider } from '../../../contexts/UsersContext';
import AddUsers from './AddUsers';
const AddUsersLauncher = () => {
const [showModal, setShowModal] = useState(false);
return (
<div>
<UsersProvider>
<Button onClick={() => setShowModal(true)}>Add Users</Button>
{showModal && <AddUsers />}
</UsersProvider>
</div>
);
};
export default AddUsersLauncher;
This all works great initially. A button is rendered and when that button is pressed then the modal is shown.
The problem lies with how to hide it. Before I was just setting isModalOpen to false in the reducer.
When I had a quick conversation with my colleague earlier today, he said that the code above would work and I wouldn't have to pass anything into AddUsers. I'm thinking though that I need to pass the setShowModal function into the component as it could then be called to hide the modal.
But I'm open to the possibility that I'm not seeing a much simpler way to do this. Might there be?
To call something on unmount you can use useEffect. Whatever you return in the useEffect, that will be called on unmount. For example, in your case
const AddUsersLauncher = () => {
const [showModal, setShowModal] = useState(false);
useEffect(() => {
return () => {
// Your code you want to run on unmount.
};
}, []);
return (
<div>
<UsersProvider>
<Button onClick={() => setShowModal(true)}>Add Users</Button>
{showModal && <AddUsers />}
</UsersProvider>
</div>
);
};
Second argument of the useEffect accepts an array, which diff the value of elements to check whether to call useEffect again. Here, I passed empty array [], so, it will call useEffect only once.
If you have passed something else, lets say, showModal in the array, then whenever showModal value will change, useEffect will call, and will call the returned function if specified.
If you want to leave showModal as state variable in AddUsersLauncher and change it from within AddUsers, then yes, you have to pass the reference of setShowModal to AddUsers. State management in React can become messy in two-way data flows, so I would advise you to have a look at Redux for storing and changing state shared by multiple components

How can I reset a react component including all transitively reachable state?

I occasionally have react components that are conceptually stateful which I want to reset. The ideal behavior would be equivalent to removing the old component and readding a new, pristine component.
React provides a method setState which allows setting the components own explicit state, but that excludes implicit state such as browser focus and form state, and it also excludes the state of its children. Catching all that indirect state can be a tricky task, and I'd prefer to solve it rigorously and completely rather that playing whack-a-mole with every new bit of surprising state.
Is there an API or pattern to do this?
Edit: I made a trivial example demonstrating the this.replaceState(this.getInitialState()) approach and contrasting it with the this.setState(this.getInitialState()) approach: jsfiddle - replaceState is more robust.
To ensure that the implicit browser state you mention and state of children is reset, you can add a key attribute to the root-level component returned by render; when it changes, that component will be thrown away and created from scratch.
render: function() {
// ...
return <div key={uniqueId}>
{children}
</div>;
}
There's no shortcut to reset the individual component's local state.
Adding a key attribute to the element that you need to reinitialize, will reload it every time the props or state associate to the element change.
key={new Date().getTime()}
Here is an example:
render() {
const items = (this.props.resources) || [];
const totalNumberOfItems = (this.props.resources.noOfItems) || 0;
return (
<div className="items-container">
<PaginationContainer
key={new Date().getTime()}
totalNumberOfItems={totalNumberOfItems}
items={items}
onPageChange={this.onPageChange}
/>
</div>
);
}
You should actually avoid replaceState and use setState instead.
The docs say that replaceState "may be removed entirely in a future version of React." I think it will most definitely be removed because replaceState doesn't really jive with the philosophy of React. It facilitates making a React component begin to feel kinda swiss knife-y.
This grates against the natural growth of a React component of becoming smaller, and more purpose-made.
In React, if you have to err on generalization or specialization: aim for specialization. As a corollary, the state tree for your component should have a certain parsimony (it's fine to tastefully break this rule if you're scaffolding out a brand-spanking new product though).
Anyway this is how you do it. Similar to Ben's (accepted) answer above, but like this:
this.setState(this.getInitialState());
Also (like Ben also said) in order to reset the "browser state" you need to remove that DOM node. Harness the power of the vdom and use a new key prop for that component. The new render will replace that component wholesale.
Reference: https://facebook.github.io/react/docs/component-api.html#replacestate
The approach where you add a key property to the element and control its value from the parent works correctly. Here is an example of how you use a component to reset itself.
The key is controlled in the parent element, but the function that updates the key is passed as a prop to the main element. That way, the button that resets a form can reside in the form component itself.
const InnerForm = (props) => {
const { resetForm } = props;
const [value, setValue] = useState('initialValue');
return (
<>
Value: {value}
<button onClick={() => { setValue('newValue'); }}>
Change Value
</button>
<button onClick={resetForm}>
Reset Form
</button>
</>
);
};
export const App = (props) => {
const [resetHeuristicKey, setResetHeuristicKey] = useState(false);
const resetForm = () => setResetHeuristicKey(!resetHeuristicKey);
return (
<>
<h1>Form</h1>
<InnerForm key={resetHeuristicKey} resetForm={resetForm} />
</>
);
};
Example code (reset the MyFormComponent and it's state after submitted successfully):
function render() {
const [formkey, setFormkey] = useState( Date.now() )
return <>
<MyFormComponent key={formkey} handleSubmitted={()=>{
setFormkey( Date.now() )
}}/>
</>
}
Maybe you can use the method reset() of the form:
import { useRef } from 'react';
interface Props {
data: string;
}
function Demo(props: Props) {
const formRef = useRef<HTMLFormElement | null>(null);
function resetHandler() {
formRef.current?.reset();
}
return(
<form ref={formRef}>
<input defaultValue={props.data}/>
<button onClick={resetHandler}>reset</button>
</form>
);
}

Resources