Can I call React Hook inside a function? - reactjs

I want to change a state using React Hook and also redirect to a new page with passing the changed state to other component after clicking a Button. So I did:
const MovieCard = (props) => {
const [movie, setMovie] = useState({})
function handleClick(movie) {
setMovie(movie)
props.history.push({
pathname: '/otherPage',
data: movie
})
}
return(
...
<Button onClick={() => handleClick(props.movie)} />
...
)
}
But Hook didn't update the state. Because based on React Hook docs, it would works like this:
<Button onClick={() => setMovie(props.movie)} />
Is it possible to call a hook inside a function or there is any way to do it?

It is possible to do what you are doing, however by changing to another page you are clearing the state that was stored on that component. You either need to not redirect (which I'm assuming you don't want to do) or you need to pass on that state to the other component you are redirecting to.

Related

Updated State using React Hooks not causing a re-render

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.

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

Updating Parents state from Child without triggering a rerender of Child in React

So I'm trying to build a single page app in react.
What I want:
On the page you can visit different pages like normal. On one page (index) i want a button the user can click that expands another component into view with a form. This component or form should be visible on all pages once expanded.
The Problem:
The index page loads some data from an api, so when the index component gets mounted, an fetch call is made. But when the user clicks the "Expand form"-Button, the state of the Parent component gets updated as expected, but the children get rerendered which causes the index component to fetch data again, which is not what I want.
What I tried
// Parent Component
const App => props => {
const [composer, setComposer] = useState({
// ...
expanded: false,
});
const expandComposer = event => {
event.preventDefault();
setComposer({
...composer,
expanded: true
});
return(
// ...
<Switch>
// ...
<Route
exact path={'/'}
component={() => (<Index onButtonClick={expandComposer}/>)}
// ....
{composer.expanded && (
<Composer/>
)};
);
};
// Index Component
const Index=> props => {
const [isLoading, setIsLoading] = useState(true);
const [data, setData] = useState([]);
useEffect(()=> {
// load some data
}, []);
if(isLoading) {
// show spinner
} else {
return (
// ...
<button onClick={props.onButtonClick}>Expand Composer</button>
// ...
);
};
};
So with my approach, when the button is clicked, the Index component fetched the data again and the spinner is visible for a short time. But I dont want to remount Index, or at least reload the data if possible
Two problems here. First, React will by default re render all child components when the parent gets updated. To avoid this behavior you should explicitly define when a component should update. In class based components PureComponent or shouldComponentUpdate are the way to go, and in functional components React.memo is the equivalent to PureComponent. A PureComponent will only update when one of it's props change. So you could implement it like this:
const Index = () =>{/**/}
export default React.memo(Index)
But this won't solve your problem because of the second issue. PureComponent and React.memo perform a shallow comparison in props, and you are passing an inline function as a prop which will return false in every shallow comparison cause a new instance of the function is created every render.
<Child onClick={() => this.onClick('some param')} />
This will actually create a new function every render, causing the comparison to always return false. A workaround this is to pass the parameters as a second prop, like this
<Child onClick={this.onClick} param='some param' />
And inside Child
<button onClick={() => props.onClick(props.param)} />
Now you're not creating any functions on render, just passing a reference of this.onClick to your child.
I'm not fully familiar with your style of React, I do not use them special state functions.
Why not add a boolean in the parent state, called "fetched".
if (!fetched) fetch(params, ()=>setState({ fetched: true ));
Hope this helps
Silly me, I used component={() => ...} instead of render={() => ...} when defining the route. As explained in react router docs, using component always rerenders the component. Dupocas' answer now works perfectly :)

how to maintain state of an ajax request and its loading state of a React component after it unmounts

I was wondering if there's a way to persist state of a component that will unmount and possibly remount in the example of a dropdown that has a download option, which will then close the dropdown upon selection, which will cause the option to unmount and a spinner will load.
Right now, I'm keeping all of the state of the related dropdown option in its parent and passing it back down to the option via props and would rather have that logic stored in a custom hook or something cleaner.
const Parent = () => {
const [isLoading, setIsLoading] = useState(false);
const [setDataToDownload, dataToDownload] = useState();
return (
<>
{isLoading && <Spinner />}
<Dropdown>
{({ close }) =>
options.map(op => (
<ChildOption
isLoading={isLoading}
setDataToDownload={setDataToDownload}
dataToDownload={dataToDownload}
setIsLoading={setIsLoading}
close={close}
op={op}
/>
))
}
</Dropdown>
</>
);
};
const ChildOption = ({ close, isLoading, setIsLoading }) => {
return (
<div
onClick={async () => {
close();
setIsLoading(true);
const data = await fetchSomeStuff();
setDataToDownload(data);
setIsLoading(false);
}}
>
{op.name}
</div>
);
};
if there's a way to persist state of a component that will unmount.
States of components are destroyed, when they are unmounted. So, you cannot access or retrieve state of an unmounted component. Unless, you store the state somewhere else.
You can use Redux, as a global state. You can dispatch an action from componentWillUnmount lifecycle method to store your current state in Redux store. And, you can retrieve it back in componentDidMount lifecycle method.
Other than Redux, you can use the localStorage to store component's state. Make use of componentWillUnmount and componentDidMount lifecycle methods as I explained above.

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