Updated State using React Hooks not causing a re-render - reactjs

I am currently making a UI where I want to display the results of a collection of NFT's that I grab using the Alchemy SDK. I am currently displaying a default collection of NFTs using getServerSideProps and passing down the result of the API call as props to the main Home component. The way I have structured my application is that I have a main Home component that displays a Gallery component where I pass down the collection of NFT's in the state of the parent (Home) component. The Gallery component then renders a Card component for each NFT by mapping through it's state and accessing properties of each NFT object (image and tokenId).
I am currently trying to implement a search bar function where users can input a contract address and the results of the collection will be updated depending on the collection that the user searched for. I can currently get the state of the parent component to be updated depending on the user search input and this is reflected in the state when I open up the React Dev Tools. I can also pass this updated state down to the childComponent. However, the Gallery Component (Gallery) does not re-render because although the props have been updated, the changes are not reflected in it's state even after implementing the useEffect method within the component. I am wondering how to change the state of the Child component (Gallery) using the updated props and render those changes in the Gallery.
Parent Component
I am currently updating the state of the Home Component in the handleSubmit() function which is an async function that runs when a user submits their search input.
These changes are currently reflected in the state of the parent component and passed down as props to the Gallery component
const Home = ({results}: {results: object}) => {
const [nfts, setNfts] = useState(results.nfts);
const [userInput, setUserInput] = useState('');
useEffect(() => {
setNfts(results.nfts);
}, [])
const handleSubmit = async(e) => {
e.preventDefault();
const newCollection = await findNftsByContractAddress(userInput);
const newResults = JSON.parse(JSON.stringify(newCollection))
setNfts(newResults.nfts);
}
return (
<div>
<Head>
<title>Shibuya Take Home</title>
</Head>
<NavBar />
<form action="submit" >
<input type="text" className="search-bar" onChange={e => setUserInput(e.target.value)} />
<input type="submit" onClick={handleSubmit}/>
</form>
{/* Search Bar Component Goes Here */}
{/* Title text */}
<Gallery key={nfts} nfts={nfts}/>
</div>
);
};
export default Home;
export const getServerSideProps = async() => {
// function is going to need to take in user input and replace the bored ape yacht club with user address
const boredApeYachtClubRaw = await findNftsByContractAddress("0xed5af388653567af2f388e6224dc7c4b3241c544");
const results = JSON.parse(JSON.stringify(boredApeYachtClubRaw))
return {props: {
results
}}
}
Gallery Component (Child)
The current issue is that I cannot get the updated props to reflect in the Gallery component. The state is never changed and thus the components on the page never re-render and display the original collection of NFTs that were passed down on the first render.
I can get the changes to be reflected if I directly render the cards using the props passed down to the child component --> but this is not a good practice as some collection will have missing elements.
I eventually need to create a function to filter through the props and create a new collection (array) that only contain NFT's with valid images (nft.media[0].gateway sometimes returns undefined)
export default function Gallery({nfts}: {nfts: object[]}) {
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
const [collection, setCollection] = useState([]);
useEffect(() => {
setTitle(nfts[0].contract.name);
setDescription(nfts[0].description);
setCollection(nfts);
}, [title, description, collection])
return(
<div className="gallery-container">
<h1 className="gallery-title">{nfts[0].contract.name}</h1>
<p className="gallery-description">{nfts[0].description}</p>
<div className="cards-container">
{nfts.map(nft => {
return (
<Card image={nft.media[0].gateway} tokenId={nft.tokenId}/>
)
})}
</div>
</div>
)
}
NFT object that I get back
0:
contract:
{address: '0xed5af388653567af2f388e6224dc7c4b3241c544', name: 'Azuki', symbol: 'AZUKI', totalSupply: '10000', tokenType: 'ERC721'}
description
:
""
media
:
[{…}]
rawMetadata
:
{name: 'Azuki #0', image: 'https://ikzttp.mypinata.cloud/ipfs/QmYDvPAXtiJg7s8JdRBSLWdgSphQdac8j1YuQNNxcGE1hg/0.png', attributes: Array(7)}
timeLastUpdated
:
"2022-10-30T14:58:51.119Z"
title
:
"Azuki #0"
tokenId
:
"0"
tokenType
:
"ERC721"
tokenUri
:
{raw: 'https://ikzttp.mypinata.cloud/ipfs/QmQFkLSQysj94s5GvTHPyzTxrawwtjgiiYS2TBLgrvw8CW/0', gateway: 'https://ipfs.io/ipfs/Qm

The problem is in the dependency array you're passing into the useEffect hook.
You're passing in the state variables within the child Gallery component, each of which is getting the actual values from the parent Home component, most likely after the first render.
By the time the child component mounts, they still have the values they are initialized with. The useEffect hook is waiting for these state variables to change before triggering again, which never happens because the logic to update them is inside the hook itself.
What you should have in the dependency array is the nfts array.
useEffect(() => {
setTitle(nfts[0].contract.name);
setDescription(nfts[0].description);
setCollection(nfts);
}, [nfts]);
Now, we could argue that the props for the child component (and hence nfts) would be available on initial mount, but it is not clear from your code as to where the nfts array actually comes from. It is passed to the parent Home component inside its props, so it could be coming from an API response elsewhere, in which case there's got to be a delay before the array is available.
It's surprising that your app does not crash when useEffect is running for the first time with stuff like nfts[0].contract.name, but again, I don't know how results is initialized.

Related

Toast Editor UI - doesn't re-render initialValue when Parent state changes

So I'm making a write component which can be used for both create, and update the notice.
the write component has a Toast-UI editor to write/update the notice.decs
I create [form, setForm] = useState({desc="defaul"}
after first render, useEffect call API with params of noticeID to get data, then setForm(data).
Form is taken into Editor as props.
After useEffect API call, the Form is updated, Toast-UI editor props updated, but the Initial value doesn't change.
Please does anybody have any thoughts on why this happens and how to fix?
Write (parent component)
const [form, setForm] = useState({
ttl: "",
desc: "DEFAULT",
});
UseEffect( Fetch API then setForm(res)
return (
<ToastEditor
onChangeForm={onChangeForm}
form={form}
setForm={setForm}
/>
);
Toast-UI editor component
const { form, setForm, onChangeForm } = props;
return (
<Editor
initialValue={form?.desc ?? ""}
/>
);
Grand children component
export class Editor extends Component<EditorProps> {
getInstance(): ToastuiEditor;
getRootElement(): HTMLElement;
}
I expect the Toast Editor to render the data instead of just the first time initital State Form

How to pass a method into a child element if the method relies on state variables in React

I've been learning react over the last few days and for the most part it makes sense, however there is one thing that is stumping me.
If I have
A Parent element with some state variables and a callback method
A child element that takes a callback method as a prop
The callback method relies on some piece of state that is in the parent element
I don't want to re-create the view object every time any state changes
Every time I try to do this, it seems like the child element is calling some older version of the parent element (presumably the instance of the parent that actually created the child) instead of the current version.
I'm getting the feeling that what I want is just wrong on a fundamental level and isnt The React Way
The reason that I am trying to do this is that my main parent contains 17 divs, each of which represent a key on a musical instrument, and each of which contains at least 20-30 divs. The lowest div (of which there are at least a few hundred) has an onClick event that I want to modify the functionality of based on whether modifier keys are held down (shift, control etc).
Currently I have Raised the state of the shiftPressed to be on the single parent element then passed down the value of that into each child through props, however re-rendering hundreds of divs whenever a user pushes shift takes quite a while.
I've made a code sandbox to show the current problem sandbox
Sandbox code:
import "./styles.css";
import { useState, useEffect, useRef } from "react";
export default function App() {
//Our state holding data
const [state, setState] = useState(false);
//Our state holding the view
const [view, setView] = useState(<div></div>);
const printState = useRef(null);
//Component did mount hook
useEffect(() => {
reGenerate();
}, []);
//state update hook
useEffect(() => {
printState.current();
}, [state]);
//function to flip the state
const flipState = () => {
setState(!state);
};
//The method that updates the view
//(The idea being that I don't want to update the view on every state change)
const reGenerate = () => {
setView(
<>
<p>
State: {state && "true"} {state || "false"}
</p>
<Child callback={printState} />
</>
);
};
//Method for validation
printState.current = () => {
console.log("Printed state: " + state);
};
return (
<div className="App">
<h1>Parent-child-prop-problem (prop-lem)</h1>
<ol>
<li>click "force regeneration"</li>
<li>
click "flip state" and the value of state after the flip will be
printed in console, but it won't show up on the HTML element
</li>
<li>
Click "print state (from child)" and observe that the console is
printing the old version of the state
</li>
</ol>
<button onClick={flipState}>Flip State</button>
<button onClick={reGenerate}>Force Regeneration</button>
{view}
</div>
);
}
function Child(props) {
return (
<div>
<button onClick={props.callback.current}>Print State (from child)</button>
</div>
);
}
Taking a quick peek at your sandbox code and I see that you are storing JSX in state, which is anti-pattern and often leads to stale enclosures like you describe.
I don't want to re-create the view object every time any state changes
"Recreating" the view is a necessary step in rendering UI in React as a result of state or props updating. State should only ever store data and the UI should be rendered from the state. In other words, treat your UI like a function of state and props. Toggle the state state value and render the UI from state.
Example:
export default function App() {
//Our state holding data
const [state, setState] = useState(false);
const printState = useRef(null);
//state update hook
useEffect(() => {
printState.current();
}, [state]);
//function to flip the state
const flipState = () => {
setState(!state);
};
//Method for validation
printState.current = () => {
console.log("Printed state: " + state);
};
return (
<div className="App">
<h1>Parent-child-prop-problem (prop-lem)</h1>
<ol>
<li>
click "flip state" and the value of state after the flip will be
printed in console, but it won't show up on the HTML element
</li>
<li>
Click "print state (from child)" and observe that the console is
printing the old version of the state
</li>
</ol>
<button onClick={flipState}>Flip State</button>
<p>State: {state ? "true" : "false"}</p>
<Child callback={printState} />
</div>
);
}
function Child(props) {
return (
<div>
<button onClick={props.callback.current}>Print State (from child)</button>
</div>
);
}
It's also generally considered anti-pattern to use any sort of "forceUpdate" function and is a code smell. In almost all circumstances if you hit a point where you need to force React to rerender you are doing something incorrect. This is the time you step back and trace through your code to find where a certain piece of state or some prop isn't updated correctly to trigger a rerender naturally.

How to get data of input fields from previous page when next page is loaded?

when "Search" is clicked, next page is loaded by the below code.
import React from "react";
import { Link } from "react-router-dom";
function name(){
return(
<div className="search">
<input type="text"placeholder="city name"></input>
<input type="text"placeholder="number of people"></input>
<p><Link to="/nextpage">Search</Link</p>
</div>
)
}
I want to take data of these input fields to fetch api using that data to make cards on next page.
How to do it?
Here's the general idea of how you could accomplish this. You need to store the form input (in your case, the city and number of people) in application state. You can do that using the useState hook. Now this state can be managed by your first component (the one which renders the input fields) and accessed by the second component (the one that will display the values).
Because the values need to be accessed by both components, you should store the state in the parent component (see lifting state up). In my example, I used App which handles routing and renders FirstPage and SecondPage. The state values and methods to change it are passed as props.
Here's how you initialise the state in the App component:
const [city, setCity] = useState(null);
const [peopleCount, setPeopleCount] = useState(null);
city is the value, setCity is a function which enables you to modify the state. We will call setCity when the user makes a change in the input, like this:
export const FirstPage = ({ city, setCity, peopleCount, setPeopleCount }) => {
...
const handleCityChange = (e) => {
setCity(e.target.value);
};
...
<input
type="text"
placeholder="city name"
value={city}
onChange={handleCityChange}
/>
When a change in the input is made, the app will call the setCity function which will update the state in the parent component. The parent component can then update the SecondPage component with the value. We can do it simply by passing the value as a prop:
<NextPage city={city} peopleCount={peopleCount} />
Now you can do whatever you want with the value in your SecondPage component. Here's the full code for it, where it just displays both values:
export const NextPage = ({ city, peopleCount }) => {
return (
<div>
<p>city: {city}</p>
<p># of people: {peopleCount}</p>
</div>
);
};
And here's the full working example: https://stackblitz.com/edit/react-uccegh?file=src/App.js
Note that we don't have any field validation, and we have to manually write the change handlers for each input. In a real app you should avoid doing all this by yourself and instead use a library to help you build forms, such as Formik.

Why React is rendering parent element, even if changed state isn't used in jsx? (Using React Hooks)

Im doing a React small training app using Hooks. Here's the example:
There is a MainPage.js and it has 3 similar child components Card.js. I have global state in MainPage and each Card has its own local state. Every Card has prop "id" from MainPage and clickButton func.
When I click button in any Card there are 2 operations:
Local variable 'clicked' becomes true.
The function from parent component is invoked and sets value to global state variable 'firstCard'.
Each file contains console.log() for testing. And when I click the button it shows actual global variable "firstCard", and 3x times false(default value of variable "clicked" in Card).
It means that component MainPage is rendered after clicking button ? And every Card is rendered too with default value of "clicked".
Why MainPage componenet is rendered, after all we dont use variable "firsCard", except console.log()?
How to make that after clicking any button, there will be changes in exactly component local state, and in the same time make global state variable "firstCard" changed too, but without render parent component(we dont use in jsx variable "firstCard")
Thanks for your help !
import Card from "../Card/Card";
const Main = () => {
const [cards, setCards] = useState([]);
const [firstCard, setFirstCard] = useState(null);
useEffect(() => {
setCards([1, 2, 3]);
}, []);
const onClickHandler = (id) => {
setFirstCard(id);
};
console.log(firstCard); // Showing corrrect result
return (
<div>
{cards.map((card, i) => {
return (
<Card
key={Date.now() + i}
id={card}
clickButton={(id) => onClickHandler(id)}
></Card>
);
})}
</div>
);
};
import React, { useState } from "react";
const Card = ({ id, clickButton }) => {
const [clicked, setClicked] = useState(false);
const onClickHandler = () => {
setClicked(true);
clickButton(id);
};
console.log(clicked); // 3x false
return (
<div>
<h1>Card number {id}</h1>
<button onClick={() => onClickHandler()}> Set ID</button>
</div>
);
};
export default Card;
You have wrong idea how react works.
When you change something in state that component will re render, regardless if you use that state variable in render or not.
Moreover, react will also re render all children of this component recursively.
Now you can prevent the children from re rendering (not the actual component where state update happened though) in some cases, for that you can look into React.memo.
That said prior to React hooks there was a method shouldComponentUpdate which you could have used to skip render depending on change in state or props.

Adding a component to the render tree via an event handler, the component doesn't seem to receive new props. Why is this?

I have a context provider that I use to store a list of components. These components are rendered to a portal (they render absolutely positioned elements).
const A = ({children}) => {
// [{id: 1, component: () => <div>hi</>}, {}, etc ]
const [items, addItem] = useState([])
return (
<.Provider value={{items, addItem}}>
{children}
{items.map(item => createPortal(<Item />, topLevelDomNode))}
</.Provider>
)
}
Then, when I consume the context provider, I have a button that allows me to add components to the context provider state, which then renders those to the portal. This looks something like this:
const B = () => {
const {data, loading, error} = useMyRequestHook(...)
console.log('data is definitely updating!!', data) // i.e. props is definitely updating!
return (
<.Consumer>
{({addItem}) => (
<Button onClick={() => {
addItem({
id: 9,
// This component renders correctly, but DOESN'T update when data is updated
component: () => (
<SomeComponent
data={data}
/>
)
})
}}>
click to add component
</Button>
)}
</.Consumer>
)
}
Component B logs that the data is updating quite regularly. And when I click the button to add the component to the items list stored as state in the provider, it then renders as it should.
But the components in the items list don't re-render when the data property changes, even though these components receive the data property as props. I have tried using the class constructor with shouldComponentUpdate and the the component is clearly not receiving new props.
Why is this? Am I completely abusing react?
I think the reason is this.
Passing a component is not the same as rendering a component. By passing a component to a parent element, which then renders it, that component is used to render a child of the parent element and NOT the element where the component was defined.
Therefore it will never receive prop updates from where I expected it - where the component was defined. It will instead receive prop updates from where it is rendered (although the data variable is actually not coming from props in this case, which is another problem).
However, because of where it is defined. it IS forming a closure over the the props of where it is defined. That closure results in access to the data property.

Resources