I have a page where there are some components: Filters, Pagination and List (with some data from the server)
Filter component includes some selects elements, inputs, etc. and button "Find".
Pagination component includes page switchers buttons and a selector for a direct page and a select to choose how many elements we should show on a page
List component shows data from the server API
Also, I have a Parent component that has all of them and a common state. (I will simplify fetch logic, but the main point that it is sent a request with 2 objects as data)
const reducer = (options, newOptions) => ({
...options,
...newOptions
})
const ParentComponent = () => {
const [filterOptions, setFilterOptions] = useReducer(
reducer,
{
filterOption1: "",
filterOption2: ""
....
}
)
const [pagingOptions, setPagingOptions] = useReducer(
reducer,
{
pageNumber: 1,
elementsPerPage: 10,
totalElement: 100
}
)
const doFetch = () => {
fetch('https://example.com', {
body: {filters: filterOptions, paging: pagingOptions}
})
}
return (
<>
<Filters
filterOptions={filterOptions}
onFilterOptionsChange={setFilterOptions}
onFindClick={doFetch}
/>
<Pagination
pagingOptions={pagingOptions}
onPagingOptionsChange={setPagingOptions}
/>
<List data={data} />
</>
)
}
When we change some filters, data in filterOptions changes because we put setFilterOptions dispatch in to Filters component (I did not show what is inside Filters component, the main point is that we use setFilterOptions({filterOption1: 'new_value'}) to change filterOptions when some filter select or input are changed) and when we click on the Find button inside Filters we use the method doFetch and do fetch with new values of filterOptions and default values of pagingOptions
After that, when data comes, we put it into List component and show it on a screen. Again I simplified it and write just <List data={data} />
My question is: How should I implement the logic for page changing. For Filters I have just one Find button to use fetch, but for paging, there are a lot of selectors and buttons that can change some of pagingOptions. And after we change a pagingOptions (a page or an elementsPerPage) I should immediately fire doFetch() method
I used useEffect
useEffect(() => {
doFetch()
}, [pagingOptions])
and see when pagingOptions changes then fire doFetch(), but eslint hooks warning told me that I should add doFetch in deps (I agree) but then it was a message in the console that doFetch should be in useCallback(), but if I wrapped doFetch() in useCallback() console told me that I should add filterOptions and pagingOptions in useCallback deps (because I use them in fetch) and after that, I got infinity loop of fetching api in network
Related
I have a modal in which a user selects to see more similar (lets say for simplicity products). If there is none, I set state to false and a noresults modal component is shown. the problem is when user AGAIN selects and there is again no products, the boolean is already set to false from before, so since no different value is provided for usestate, the component doesnt re render. However I want to show the modal popup with no results info every time. How can I make the component re render without using bad practises such a force update etc? Thank you
const App = ({route}) => {
const [more, setMore] = useState(true);
const findItem = (id) => { //triggered from a popup
const res = API.find...
if (res) {
setMore(true)
} else {
setMore(false)
}
}
return (
{!more ? <NoResults /> : <Results />}
)
}
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.
I have below component which gets data expenses as props,
Initial reader will show table with expenses values, but when row is deleted new data are stored in newExpenses which need to use in DataGrid
const DataTable = ({ expenses }) => {
const handleDeleteCompleted = async () => {
// Function for delete
const newExpenses = await deleteExpense(fetchExpenseId)
setOpenConfirmDelete(false);
enqueueSnackbar(t('The user account has been removed'), {
variant: 'success',
anchorOrigin: {
vertical: 'top',
horizontal: 'right'
},
TransitionComponent: Zoom
});
};
return (
<>
<div style={{ height: 800, width: "100%" }}>
<DataGrid
rows={expenses}
columns={columns}
/>
</div>
</>
);
};
export default DataTable;
Now I want to update DataGrid rows with newExpenses which are fetch with deleteExpense function.
How can Irerender DataGrid with new data?
Thank youm
You're duplicating state and confusing where your system of record is. The first thing you need to do is decide:
Should the expenses state be managed in this component, or should it be managed in a parent and passed to this component?
Don't mix the two.
If the answer is that state should be managed in this component, remove the prop. Within this component you would fetch the data (perhaps once initially in a useEffect for example), store it in useState, and update that state accordingly (which will trigger a re-render).
If the answer is that state should be managed in a parent component, then that component needs to perform the updates/deletes/etc. It can pass functions to do that as additional props for this component to use. As that parent component updates its state, that would trigger a re-render of this child component.
When you need to rerender a component based on data, you need to put that data into state. Whenever React state changes, the component gets rerendered.
In your case you'd need to put the prop expenses inside state.
import { useState } from 'react';
const DataTable = ({ expenses: initialExpenses }) => {
const [expenses, setExpenses] = useState(initialExpenses);
...
// On handler
const newExpenses = await deleteExpense(fetchExpenseId);
setExpenses(newExpenses);
}
That will make sure that your component gets rerendered.
This is the case in which the prop is the initial data, because you can't update the components props, you can only use them. In this example, your component is managing the state.
In the case where the parent component needs to manage the state, you'd need to not only pass expenses as a prop, but a callback to update it. Whenever a React prop changes, the component also rerenders. In this case you'd pass in both expenses and onDeleteExpense (for example). The parent component is the one who defines onDeleteExpense and handles the state (using useState).
const Parent = () => {
const [expenses, setExpenses] = useState(someInitialValue);
const onDeleteExpense = (expenseId) => {
const newExpenses = await deleteExpense(expenseId);
setExpenses(newExpenses);
}
return <DataTable expenses={expenses} onDeleteExpenses={onDeleteExpenses} />;
}
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.
I'm currently migrating to antd, and have a modal appear on a certain route (ie /userid/info). I'm able to achieve this if I use the antd Modal react component, but I'd like to be able to use the modal methods provided such as Modal.confirm,Modal.info and Modal.error as they offer nicer ui straight out of the box.
I'm running to multiple issues such as having the modal rendered multiple times (both initially and after pressing delete in the delete user case), and unable to make it change due to state (ie display loading bar until data arrives). This is what i've tried but it constantly renders new modals, ive tried something else but that never changed out of displaying <Loader /> even though isFetching was false. I'm not sure what else to try.
const UserInfoFC: React.FC<Props> = (props) => {
const user = props.user.id;
const [isFetching, setIsFetching] = React.useState<boolean>(true);
const [userInfo, setUserInfo] = React.useState<string>('');
const modal = Modal.info({
content: <Loader />,
title: 'User Info',
});
const displayModal = () => {
const renderInfo = (
<React.Fragment>
<p>display user.info</p>
</React.Fragment>
);
const fetchInfo = async () => {
try {
user = // some api calls
setUserInfo(user.info);
modal.update({ content: renderInfo })
} catch (error) {
// todo
}
setIsFetching(false);
};
fetchInfo();
};
displayModal();
return(<div />);
};
reference: https://ant.design/components/modal/#components-modal-demo-confirm
edit: here is a replication of one of the issues I face: https://codesandbox.io/embed/antd-reproduction-template-1jsy8
As mentioned in my comment, you can use a useEffect hook with an empty dependency array to run a function once when the component mounts. You can initiate an async call, wait for it to resolve and store the data in your state, and launch a modal with a second hook once the data arrives.
I made a sandbox here
Instead of going to /:id/info and routing to a component which would have returned an empty div but displayed a modal, I created a displayInfo component that displays a button and that controls the modal. I got rid of attempting to use routes for this.
What I have now is similar to the docs