React seems to re-render the wrong component when triggering another one - reactjs

I have two components in my project.
One is Aapp.jsx
One is Child.jsx
When I called the state set function in Child 1; it is supposed to see mentioning child 1 in the console, but now it is showing child 3. It is weird and why is that?
I guess the reason is "key" in prop, so I added "key" there too. But the problem is still there.
Here is the code:
App:
import React,{useState,useEffect} from 'react';
import {Child} from './Child.jsx'
export function App(props) {
[message,setMessage]=useState('');
[showChild1,setShowChild1]=useState(true);
[showChild2,setShowChild2]=useState(true);
[showChild3,setShowChild3]=useState(true);
[child1data,setChild1data] = useState('child1');
[child2data,setChild2data] = useState('child2');
[child3data,setChild3data] = useState('child3');
useEffect(() => {
console.log('parent was rendered')
})
return (
<div className='App'>
<button onClick={()=>setShowChild1(!showChild1)}>Show child1</button>
{showChild1 && <Child key='1' data={child1data}/>}
<br/>
<br/>
<button onClick={()=>setShowChild2(!showChild2)}>Show child2</button>
{showChild2 && <Child key='2'data={child2data}/>}
<br/>
<br/>
<button onClick={()=>setShowChild3(!showChild3)}>Show child3</button>
<br/>
{showChild3 && <Child key='3' data={child3data}/>}
</div>
);
}
// Log to console
console.log('Hello console')
Child:
import React, {useState, useEffect} from 'react';
export const Child = (props) => {
const {data} = props;
[message,setMessage]=useState('');
useEffect(()=>{
console.log(data)
console.log(message)
})
return <>
<h1>This is {data}</h1>
<input onChange={((e)=>setMessage(e.target.value))}></input>
</>
}
For better illustrate, here is my code
https://playcode.io/940717

This is a tricky one but fortunately comes with a very simple fix.
TL;DR
In Child.jsx change this:
[message, setMessage] = useState('');
to this:
const [message, setMessage] = useState('');
The longer answer
When declaring variables without let, const, or var (in a non-strict environment) Javascript will create an implicit global variable. What this means is that your message and setMessage variables point to the last value they were assigned. I.e. The global variable message will be assigned to the result of the useState call in the last Child component that you render, which in your case is Child 3.
So, when you modify message, Child 3 detects that change and runs the effect for Child 3, not the Child component where the change was actually made.
You can see this in action by changing the Child component to this and examining the output in the console:
[message, setMessage] = useState("");
useEffect(() => {
console.log(data); // Will always log "child 3"
console.log(message);
});
return (
<>
<h1>This is {data}</h1>
<input
onChange={(e) => {
console.log(data); // Will log "child n" where n is the child you expect
setMessage(e.target.value);
}}
></input>
</>
);
You might also be curious as to why, if all of your Child components are referencing the same message variable, you're still able to change the inputs individually. This is because you're input elements are uncontrolled and their state is being managed by the browser natively.
You can see this in action by adding the following to the input element in your Child component:
<input value={message} onChange={e => setMessage(e.target.value)}></input>
What to do in the future
You should always, always, always declare your variables with let or const as it will save you headaches like this in the future. In fact, many modern transpilers such as Babel will throw an error if you don't.
When using uncontrolled inputs you should consider asking yourself why you need to do so. More often than not, a controlled input is what you actually want and in the cases where it's not, consider leaving a comment as to why it's not.

You forgot const in many places:
const [message, setMessage] = useState('');
Without the const keyword message and setMessage (and other stuff) leak to the window global object.
Working example

Related

React idiomatic controlled input (useCallback, props and scope)

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.

In react world, when does props changing happen? It's definitely triggered by parent component re-rendering?

When it comes to thinking about possibility of props changing, should I only care about parent component updating? Any other possible cases exist for props changing?
I can guess only one case like the code below. I would like to know if other cases exist.
const ParentComponent = () => {
const [message, setMessage] = useState('Hello World');
return (
<>
<button onClick={() => setMessage('Hello Foo')}>Click</button>
<ChildComponent message={message} />
</>
);
};
const ChildComponent = ({ message }: { message: string }) => {
return <div>{message}</div>;
};
Props can change when a component's parent renders the component again with different properties. I think this is mostly an optimization so that no new component needs to be instantiated.
Much has changed with hooks, e.g. componentWillReceiveProps turned into useEffect+useRef (as shown in this other SO answer), but Props are still Read-Only, so only the caller method should update it.

React state not in sync with the actual state

I have a very simple setup. I just have one controlled input and I want to console.log the input.
import React, {useState} from 'react'
const App = () => {
const [text, setText] = useState('')
const handleChange = event => {
event.preventDefault()
setText(_prev => event.target.value)
consoel.log(text)
}
return(
<div>
<input type="text" value={text} onChange={handleChange} />
</div>
)
}
I seem to only be getting the one before the actual state. For example if I type 'abc' in the console i only see 'ab' and after typing a fourth character I see 'abc'. Why is my state always one input behind?
(trying to change the state in the following manner setText(event.target.value) has provided the same results).
Due to it's asynchronous behavior you can't directly get value right after it's being updated. To get the value, you can use the effect hook which will check for the change and acts upon the behavior:
useEffect(() => {
console.log(text) // act upon
}, [text]) // when text changes, it runs
Also, side note, you just have to do:
setText(event.target.value)

React access state after render with functional components

I'm a bit of a newbie with React functional components, I have a child and parent components with some state that gets updated with useEffect, which state apparently resets back to its initial values after render.
Parent has a list of users it passes to its child:
Parent:
const Parent = () => {
const [users, setUsers] = useState([])
const getUsers = () => {
setUsers(["pedro", "juan"])
}
useEffect(() => {
getUsers()
}, []);
return <div>
<Child users={users} />
}
Child:
const Child = () => {
const [users, setUsers] = useState([])
useEffect(() => {
setUsers(props.users)
}, [[...props.users]]);
}
If I for any reason try to access state (users) from either my child or parent components I get my initial value, which is an empty array, not my updated value from getUsers(), generally with a Parent Class component I'd have no trouble accessing that info, but it seems like functional components behave diffently? or is it caused by the useEffect? generally I'd use a class component for the parent but some libraries I use rely on Hooks, so I'm kind of forced to use functional components.
There are a couple of mistakes the way you are trying to access data and passing that data.
You should adapt to the concept of lifting up state, which means that if you have users being passed to your Child component, make sure that all the logic regarding adding or removing or updating the users stays inside the Parent function and the Child component is responsible only for displaying the list of users.
Here is a code sandbox inspired by the code you have shared above. I hope this answers your question, do let me know if otherwise.
Also sharing the code below.
import React, { useState } from "react";
export default function Parent() {
const [users, setUsers] = useState([]);
let [userNumber, setUserNumber] = useState(1); // only for distinctive users,
//can be ignored for regular implementation
const setRandomUsers = () => {
let newUser = {};
newUser.name = `user ${userNumber}`;
setUsers([...users, newUser]);
setUserNumber(++userNumber);
};
return (
<div className="App">
<button onClick={setRandomUsers}>Add New User</button>
<Child users={users} />
</div>
);
}
const Child = props => {
return (
props.users &&
props.users.map((user, index) => <div key={index}>{user.name}</div>)
);
};
it doesnt make sense to me that at Child you do const [users, setUsers] = useState([]). why dont you pass down users and setUsers through props? your child's setUser will update only its local users' state value, not parent's. overall, duplicating parent state all around its children is not good, you better consume it and updating it through props.
also, once you do [[...props.users]], you are creating a new array reference every update, so your function at useEffect will run on every update no matter what. useEffect doesnt do deep compare for arrays/objects. you better do [props.users].

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

Resources