Call child component based on state value in parent component using React - reactjs

I am trying to pass data to child component(grid) from parent component. In parent component, I have created a state for the data which gets updated once the data fetch from an api is finished. Currently, I check if the state length is greater than 0 and only then I call child component.
const ParentComponent()=>{
const [gridData, setGridData] = useState([]);
useEffect(() => { getDataFunction (); }
const getDataFunction = async () => {
try { //apis to get data }
setGridData(apiData);
}
return (
{gridData.length > 0 && <ChildComponent Grid tableData={gridData}}
);
}
The above code works fine. It displays the data when there is some value in gridData. But I would like to display an empty table while the api is being called. Or if the api has no data, even then an empty table needs to be displayed.
Child component:
const Grid = React.forwardRef(({ tableData, columnData, ...props}, ref) => {
let activeColumn = [...columnData];
const onGridReady = params => {
setGridApi(params.api);
setGridColumnApi(params.columnApi);
setGridRowData(tableData);
params.api.applyTransaction({ add: tableData });
}
return (
<AgGridReact
onGridReady={onGridReady}
columnDefs={activeColumn}
pagination={true}
</AgGridReact>
);
});
If I just use gridData instead of gridData.length, then even after the value of the state is updated, the table is not updated. It will always be empty even if later the data is fetched from an api. How do I make child component update when gridData state changes ?

return (
<>
{gridData.length > 0 && <ChildComponent Grid tableData={gridData} />}
{gridData.length === 0 && <div>Hi there</div>}
</>
);
You simply render it conditionally based on length

I hope you have written useEffect code properly, bcoz the syntax seems incorrect.
useEffect(() => {
const getDataFunction = async () => {
try {
setGridData(apiData);
}
}
getDataFunction()
},[])
return (
<ChildComponent Grid tableData={gridData}
);
Use gridData.length > 0 line while rendering your child component
and for Empty Table while loading you can use Skeleton on loading state.

Related

React Hook "useEffect" is called conditionally, in supposedly simple get-display-retrieve inputs component

Please help,
I am going around in circles (new to React).
I either get 'React Hook "useState" is called conditionally' and if I change it to be the beneath code,
I get 'React Hook "useEffect" is called conditionally'.
The idea is to
retrieve the data via useQuery and the db record id pased in via props.
if that data is null, e.g. the query used the id=0 (like an insert not an update of a record), then deviceObject to the empty record, else to the retrieved data.
Set 'deviceObject' into state.
i.e. The order is important, but setRow should only be called once, not multiple times, which leads to react crashing with too many renders.
export default function DeviceModal(props) {
const dataRowId = props.dataRowId;
const classes = useStyles();
const [row, setRow] = useState('')
const device = useQuery(getDevice_query, {variables: {id: dataRowId}});
if (device.loading) return <DataLoader/>;
if (device.error) return <p style={{color: 'white'}}>{("GraphQL Error " + device.error)})</p>;
// Create an empty recordObject to be populated and sent back for insert to db.
const emptyDevice = {
id : 0,
deviceId : 0,
deviceClass :{name : '',},
serialNumber: 0,
}
const deviceObject = device.data.getDevice !== null ? device.data.getDevice : emptyDevice;
useEffect(()=>{
setRow(deviceObject)
},[])
const handleSave = (value) => {
};
const HandleChange = e => {
useEffect(()=>{
setRow({...row, [e.target.name]: e.target.value })
},[])
};
return (
<div>
<Modal ...>
<div className={classes.paper}>
<Grid container direction="row" justify="center" alignItems="center">
<Grid item xs={4}>
<TextField
id="deviceId"
name="deviceId"
defaultValue={row.deviceId}
onChange={HandleChange}
/>
</Grid>
</Grid>
{/* 30 other textfields to capture*/}
....
</div>
</Modal>
</div>
)};
Edit as per Long Nguyen:
// Set the device object record to be either the empty record, or the records retrieved from db if those are populated (not null)
let deviceObject = {};
const Component = () => {
deviceObject = device.data.getDevice !== null ? device.data.getDevice: emptyDevice;
return <RefactorComponent />;
}
// Set the device object (empty or populated with db-retrieved rows,) into state
const RefactorComponent = () =>
{
useEffect(()=>{
setRow(deviceObject)
},[deviceObject])
// return ()
}
Component();
You make a call of hooks after condition. It makes the hook that you call does not appear in the same order between renders, because “React relies on the order in which Hooks are called”.
You can find useful information, also the way to solve your problem in this document.
https://reactjs.org/docs/hooks-rules.html#explanation
UPDATE:
You should not place hooks in a handle, of course.
If you still want to keep the original code, you can extract the block after condition to the other component like this
const Component = () => {
if(conditions) return <Other />
// 🚫 bad, this hook order is not fixed
// it can appear or not
useEffect(() => {
...the effect after condition...
});
return (...);
}
const Component = () => {
if(conditions) return <Other />
return <RefactorComponent />
}
const RefactorComponent = () => {
// ✅ ok, the hooks order is fixed
useEffect(() => {
...the effect after condition...
});
return (...);
}

react, map component, unexpected result

I am building Weather App, my idea is to save city name in database/localhost, place cities in useState(right now it's hard coded), iterate using map in first child component and display in second child component.
The problem is that 2nd child component outputs only one element (event though console.log prints both)
BTW when I change code in my editor and save, then another 'li' element appears
main component
const App = () => {
const [cities, setCities] = useState(['London', 'Berlin']);
return (
<div>
<DisplayWeather displayWeather={cities}/>
</div>
)
}
export default App
first child component
const DisplayWeather = ({displayWeather}) => {
const [fetchData, setFetchData] = useState([]);
const apiKey = '4c97ef52cb86a6fa1cff027ac4a37671';
useEffect(() => {
displayWeather.map(async city=>{
const res =await fetch(`http://api.openweathermap.org/data/2.5/weather?q=${city}&units=metric&appid=${apiKey}`)
const data = await res.json();
setFetchData([...fetchData , data]);
})
}, [])
return (
<>
{fetchData.map(data=>(
<ul>
<Weather
data={data}/>
</ul>
))}
</>
)
}
export default DisplayWeather
second child component
const Weather = ({data}) => {
console.log(data) // it prints correctly both data
return (
<li>
{data.name} //display only one data
</li>
)
}
export default Weather
The Problem
The setFetchData hooks setter method is asynchronous by default, it doesn't give you the updated value of the state immediately after it is set.
When the weather result for the second city is returned and set to state, the current value fetchData at the time is still an empty array, so you're essentially spreading an empty array with the second weather result
Solution
Pass a callback to your setFetchData and get the current previous value of the state and then continue with your spread accordingly.
Like this 👇🏽
setFetchData((previousData) => [...previousData, data]);

ReactJS how to memoize within a loop to render the same component

I have a component that creates several components using a loop, but I need to rerender only the instance being modified, not the rest. This is my approach:
function renderName(item) {
return (
<TextField value={item.value || ''} onChange={edit(item.id)} />
);
}
function renderAllNames(items) {
const renderedItems = [];
items.forEach(x => {
const item = React.useMemo(() => renderName(x), [x]);
renderedItems.push(item);
});
return renderedItems;
};
return (
<>
{'Items'}
{renderAllNames(names)};
</>
);
This yells me that there are more hooks calls than in the previous render. Tried this instead:
function renderAllNames(items) {
const renderedItems = [];
items.forEach(x => {
const item = React.memo(renderName(x), (prev, next) => (prev.x === next.x));
renderedItems.push(item);
});
return renderedItems;
};
Didn't work either... the basic approach works fine
function renderAllNames(items) {
const renderedItems = [];
items.forEach(x => {
renderedItems.push(renderName(x));
});
return renderedItems;
};
But it renders all the dynamic component everytime I edit any of the fields, so how can I get this memoized in order to rerender only the item being edited?
You're breaking the rules of hooks. Hooks should only be used in the top level of a component so that React can guarantee call order. Component memoisation should also really only be done using React.memo, and components should only be declared in the global scope, not inside other components.
We could turn renderName into its own component, RenderName:
function RenderName({item, edit}) {
return (
<TextField value={item.value || ''} onChange={() => edit(item.id)} />
);
}
And memoise it like this:
const MemoRenderName = React.memo(RenderName, (prev, next) => {
const idEqual = prev.item.id === next.item.id;
const valEqual = prev.item.value === next.item.value;
const editEqual = prev.edit === next.edit;
return idEqual && valEqual && editEqual;
});
React.memo performs strict comparison on all the props by default. Since item is an object and no two objects are strictly equal, the properties must be deeply compared. A side note: this is only going to work if edit is a referentially stable function. You haven't shown it but it would have to be wrapped in a memoisation hook of its own such as useCallback or lifted out of the render cycle entirely.
Now back in the parent component you can map names directly:
return (
<>
{'Items'}
{names.map(name => <MemoRenderName item={name} edit={edit}/>)}
</>
);

How to force component render in React

I am making a simple ajax call to an API that produces data every 5 seconds, the data is received correctly using observables, and the data state is updated as expected.
the problem is: the page is rendering only once when it is loaded, and not when date is changed using the setData function. if I reload the page by navigating to another link and then come back the data shows as expected
her is the code:
function MyComponent() {
const [data, setData] = useState(RxJSStore.getData())
useEffect(() => {
RxJSStore.addChangeListener(onChange);
if (data.length === 0) loadDataRxJSAction()
return () => RxJSStore.removeChangeListener(onChange)
}, [data.length])
function onChange() {
setData(RxJSStore.getData())
}
return (
<>
<List data={data} />
</>
)
}
There is a way around this by forcing the component to render every time the onChange method is called. It is written in the documentation that this must only be used for testing purposes, but as the situation does not have another solution this might be the solution for this use case.
import { useRefresh } from 'react-tidy'
function MyComponent() {
const [data, setData] = useState(RxJSStore.getData())
const refresh = useRefresh()
useEffect(() => {
RxJSStore.addChangeListener(onChange);
if (data.length === 0) loadDataRxJSAction()
return () => RxJSStore.removeChangeListener(onChange)
}, [data.length])
function onChange() {
setWarnings(RxJSStore.getWarnings())
refresh()
}
return (
<>
<List data={data} />
</>
)
}

Using React hooks, how can I update an object that is being passed to a child via props?

The parent component contains an array of objects.
It maps over the array and returns a child component for every object, populating it with the info of that object.
Inside each child component there is an input field that I'm hoping will allow the user to update the object, but I can't figure out how to go about doing that.
Between the hooks, props, and object immutability, I'm lost conceptually.
Here's a simplified version of the parent component:
const Parent = () => {
const [categories, setCategories] = useState([]);
useEffect(()=>{
// makes an axios call and triggers setCategories() with the response
}
return(
categories.map((element, index) => {
return(
<Child
key = {index}
id = {element.id}
firstName = {element.firstName}
lastName = {element.lastName}
setCategories = {setCategories}
})
)
}
And here's a simplified version of the child component:
const Child = (props) => {
return(
<h1>{props.firstName}</h1>
<input
defaultValue = {props.lastName}
onChange={()=>{
// This is what I need help with.
// I'm a new developer and I don't even know where to start.
// I need this to update the object's lastName property in the parent's array.
}}
)
}
Maybe without knowing it, you have lifted the state: basically, instead of having the state in the Child component, you keep it in the Parent.
This is an used pattern, and there's nothing wrong: you just miss a handle function that allows the children to update the state of the Parent: in order to do that, you need to implement a handleChange on Parent component, and then pass it as props to every Child.
Take a look at this code example:
const Parent = () => {
const [categories, setCategories] = useState([]);
useEffect(() => {
// Making your AXIOS request.
}, []);
const handleChange = (index, property, value) => {
const newCategories = [...categories];
newCategories[index][property] = value;
setCategories(newCategories);
}
return categories.map((c, i) => {
return (
<Child
key={i}
categoryIndex={i}
firstName={c.firstName}
lastName={c.lastName}
handleChange={handleChange} />
);
});
}
const Child = (props) => {
...
const onInputChange = (e) => {
props.handleChange(props.categoryIndex, e.target.name, e.target.value);
}
return (
...
<input name={'firstName'} value={props.firstName} onChange={onInputChange} />
<input name={'lastName'} value={props.lastName} onChange={onInputChange} />
);
}
Few things you may not know:
By using the attribute name for the input, you can use just one handler function for all the input elements. Inside the function, in this case onInputChange, you can retrieve that information using e.target.name;
Notice that I've added an empty array dependecies in your useEffect: without it, the useEffect would have run at EVERY render. I don't think that is what you would like to have.
Instead, I guest you wanted to perform the request only when the component was mount, and that is achievable with n empty array dependecies;

Resources