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

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 (...);
}

Related

React dynamically added components not rendered

I'm dynamically adding instances of a custom (Kendo-React) component into an array in my main App.
The component:
const PersonDD = () => {
const ages = ["Child", "Adult", "Senior"];
return (
<div>
<div>Person:</div>
<DropDownList
data={ages} style={{ width: "300px", }}
/>
</div>
);
};
I'm adding one instance on initial render, and another two instances after the result from an Ajax call returns.
const SourceTab = (SourceTabProps) => {
....
var componentList = [];
componentList.push(<PersonDD/>);
async function getStrata(){
var url = '/access/.im.read';
const res = await axios.get( url );
console.log(res.data.item);
componentList.push(<PersonDD/>);
componentList.push(<PersonDD/>);
}
React.useEffect(() =>{
getStrata();
},[]);
return (
<Title title="People" />
<div className='assignment_div_css'>
{componentList}
</div>);
};
The problem I have is that the one instance in the initial array are rendered, but the two created after the Ajax call are not rendered.
Do I need to call .render() or something similar to refresh?
You can simply use react useState to rerender component and in jsx map them.
like this :
const SourceTab = (SourceTabProps) => {
const [componentList,setComponentList] = useState([PersonDD])
async function getStrata(){
var url = '/access/.im.read';
const res = await axios.get( url );
console.log(res.data.item);
setComponentList([...componentList,PersonDD,PersonDD])
}
React.useEffect(() =>{
getStrata();
},[]);
return (
<Title title="People" />
<div className='assignment_div_css'>
{componentList.map((Component,index)=> <Component key={index} />)}
</div>);
};
You need to remember that React only re-renders (refreshes the UI/view) when a state changes. Your componentList is not a state at the moment but just an ordinary variable. make it a state by using useState hook.
Not sure if it is a bad practice or not but I haven't seen any react project that keeps an entire component as a state so instead of creating a state with an array of components, just push a data representation of the components you want to render. Then display the component list using your list and using .map
Here's how it would look like.
....
const [personList, setPersonList] = useState([1]);
async function getStrata(){
var url = '/access/.im.read';
const res = await axios.get( url );
setPersonList(state => state.push(2)); //you can make this dynamic so it can rerender as much components as you like, for now im pushing only #2
}
React.useEffect(() =>{
getStrata();
},[]);
return (
<Title title="People" />
<div className='assignment_div_css'>
{personList.map((item, key) => <PersonDD key={key} />)}
</div>);
};
Need to use the map to render a list
<div className='assignment_div_css'>
{componentList.map(component => <>{component}</>)}
</div>);
also, use a usestate to variable
const [componentList , setComponentList ]= React.useState[<PersonDD/>];
inside function set like this
console.log(res.data.item);
setComponentList(state => [...state, <PersonDD/>, <PersonDD/>]);

Incorrect use of useEffect() when filtering an array

I have this React app that's is getting data from a file showing in cards. I have an input to filter the cards to show. The problem I have is that after I filter once, then it doesn't go back to all the cards. I guess that I'm using useEffect wrong. How can I fix this?
import { data } from './data';
const SearchBox = ({ onSearchChange }) => {
return (
<div>
<input
type='search'
placeholder='search'
onChange={(e) => {
onSearchChange(e.target.value);
}}
/>
</div>
);
};
function App() {
const [cards, setCards] = useState(data);
const [searchField, setSearchField] = useState('');
useEffect(() => {
const filteredCards = cards.filter((card) => {
return card.name.toLowerCase().includes(searchField.toLowerCase());
});
setCards(filteredCards);
}, [searchField]);
return (
<div>
<SearchBox onSearchChange={setSearchField} />
<CardList cards={cards} />
</div>
);
}
you should Include both of your state "Card", "searchedField" as dependincies to useEffect method.once any change happens of anyone of them, your component will re-render to keep your data up to date,
useEffect(() => { // your code }, [searchField, cards]);
cards original state will be forever lost unless you filter over original data like const filteredCards = data.filter().
though, in a real project it's not interesting to modify your cards state based on your filter. instead you can remove useEffect and create a filter function wrapped at useCallback:
const filteredCards = useCallback(() => cards.filter(card => {
return card.name.toLowerCase().includes(searchField.toLowerCase());
}), [JSON.stringify(cards), searchField])
return (
<div>
<SearchBox onSearchChange={setSearchField} />
<CardList cards={filteredCards()} />
</div>
);
working example
about array as dependency (cards)
adding an object, or array as dependency at useEffect may crash your app (it will throw Maximum update depth exceeded). it will rerun useEffect forever since its object reference will change everytime. one approach to avoid that is to pass your dependency stringified [JSON.stringify(cards)]

How to update useRef after redux update

I'm using useRef to hold the reference of a function that is invoked from a child component (TaskItem) when it is clicked on:
UserTask -> TaskList -> TaskItem
// UserTask.js
const [detailsVisible, setDetailsVisible] = useState(false)
const [selectedTask, setSelectedTask] = useState({})
const openTask = (id) =>{
const task = props.tasks.find(task => task.id === id);
setDetailsVisible(true);
setSelectedTask(task);
}
const openTaskRef = useRef(openTask);
const openTaskHandler = useCallback(id =>{
openTaskRef.current(id);
}, []);
return (
<React.Fragment>
<TaskList
tasks={props.tasks}
onClick={openTaskHandler}
/>
<Modal visible={detailsVisible} onClose={()=> setDetailsVisible(false)}>
<Comments items={selectedTask.comments || []} />
</Modal>
</React.Fragment>
)
The list of tasks arrives via redux->props a few seconds after they are fetched from an API:
// Still UserTask.js
useEffect(()=>{
fetchTasks()
}, [fetchTasks])
const mapDispatchToProps = dispatch =>{
return {
fetchTasks: ()=> dispatch(fetchTasks())
}
}
export default connect(mapStateToProps, mapDispatchToProps)(UserTasks)
Now, my problem is that when the component (UserTask) is rendered the first time, the 'openTaskRef' gets created with a context in which props.tasks is empty (since the API request hasn't been issued yet). I need to use the useRef/useCallback combination to avoid re-rendering of TaskItems when the props.tasks list is updated after an item is removed (a scenario not described in the code above).
How can I control this scenario? How can I update the reference to openTask to include the most recent list of tasks?
Thanks.

What's the React best practice for getting data that will be used for page render?

I need to get data that will be used for the page that I'm rendering. I'm currently getting the data in a useEffect hook. I don't think all the data has been loaded before the data is being used in the render. It's giving me an error "property lastName of undefined" when I try to use it in the Chip label.
I'm not sure where or how I should be handling the collection of the data since it's going to be used all throughout the page being rendered. Should I collect the data outside the App function?
const App = (props) => {
const [teams] = useState(["3800", "0200", "0325", "0610", "0750", "0810"]);
const [players, setPlayers] = useState([]);
useEffect(() => {
teams.forEach(teamId => {
axios.defaults.headers.common['Authorization'] = authKey;
axios.get(endPoints.roster + teamId)
.then((response) => {
let teamPlayers = response.data.teamPlayers;
teamPlayers.forEach(newPlayer => {
setPlayers(players => [...players, newPlayer]);
})
})
.catch((error) => {
console.log(error);
})
});
}, []);
let numPlayersNode =
<Chip
variant="outlined"
size="small"
label={players[1].lastName}
/>
return (...
You iterate over a teamPlayers array and add them one at a time, updating state each time, but players is always the same so you don't actually add them to state other than the last newPlayer.
Convert
teamPlayers.forEach(newPlayer => {
setPlayers(players => [...players, newPlayer]);
});
to
setPlayers(prevPlayers => [...prevPlayers, ...teamPlayers]);
Adds all new players to the previous list of players using a functional state update.
You also have an initial state of an empty array ([]), so on the first render you won't have any data to access. You can use a truthy check (or guard pattern) to protect against access ... of undefined... errors.
let numPlayersNode =
players[1] ? <Chip
variant="outlined"
size="small"
label={players[1].lastName}
/> : null
You should always create a null check or loading before rendering stuff. because initially that key does not exists. For example
<Chip
variant="outlined"
size="small"
label={players.length > 0 && players[1].lastName}
/>
this is an example of a null check
For loading create a loading state.
When functional component is rendered first, useEffect is executed only after function is returned.
and then, if the state is changed inside of useEffect1, the component will be rendered again. Here is a example
import React, {useEffect, useState} from 'react'
const A = () => {
const [list, setList] = useState([]);
useEffect(() => {
console.log('useEffect');
setList([{a : 1}, {a : 2}]);
}, []);
return (() => {
console.log('return')
return (
<div>
{list[0]?.a}
</div>
)
})()
}
export default A;
if this component is rendered, what happen on the console?
As you can see, the component is rendered before the state is initialized.
In your case, error is happened because players[1] is undefined at first render.
the simple way to fix error, just add null check or optional chaining like players[1]?.lastName.

useLoopCallback -- useCallback hook for components created inside a loop

I'd like to start a discussion on the recommended approach for creating callbacks that take in a parameter from a component created inside a loop.
For example, if I'm populating a list of items that will have a "Delete" button, I want the "onDeleteItem" callback to know the index of the item to delete. So something like this:
const onDeleteItem = useCallback(index => () => {
setList(list.slice(0, index).concat(list.slice(index + 1)));
}, [list]);
return (
<div>
{list.map((item, index) =>
<div>
<span>{item}</span>
<button type="button" onClick={onDeleteItem(index)}>Delete</button>
</div>
)}
</div>
);
But the problem with this is that onDeleteItem will always return a new function to the onClick handler, causing the button to be re-rendered, even when the list hasn't changed. So it defeats the purpose of useCallback.
I came up with my own hook, which I called useLoopCallback, that solves the problem by memoizing the main callback along with a Map of loop params to their own callback:
import React, {useCallback, useMemo} from "react";
export function useLoopCallback(code, dependencies) {
const callback = useCallback(code, dependencies);
const loopCallbacks = useMemo(() => ({map: new Map(), callback}), [callback]);
return useCallback(loopParam => {
let loopCallback = loopCallbacks.map.get(loopParam);
if (!loopCallback) {
loopCallback = (...otherParams) => loopCallbacks.callback(loopParam, ...otherParams);
loopCallbacks.map.set(loopParam, loopCallback);
}
return loopCallback;
}, [callback]);
}
So now the above handler looks like this:
const onDeleteItem = useLoopCallback(index => {
setList(list.slice(0, index).concat(list.slice(index + 1)));
}, [list]);
This works fine but now I'm wondering if this extra logic is really making things faster or just adding unnecessary overhead. Can anyone please provide some insight?
EDIT:
An alternative to the above is to wrap the list items inside their own component. So something like this:
function ListItem({key, item, onDeleteItem}) {
const onDelete = useCallback(() => {
onDeleteItem(key);
}, [onDeleteItem, key]);
return (
<div>
<span>{item}</span>
<button type="button" onClick={onDelete}>Delete</button>
</div>
);
}
export default function List(...) {
...
const onDeleteItem = useCallback(index => {
setList(list.slice(0, index).concat(list.slice(index + 1)));
}, [list]);
return (
<div>
{list.map((item, index) =>
<ListItem key={index} item={item} onDeleteItem={onDeleteItem} />
)}
</div>
);
}
Performance optimizations always come with a cost. Sometimes this cost is lower than the operation to be optimized, sometimes is higher. useCallback it's a hook very similar to useMemo, actually you can think of it as a specialization of useMemo that can only be used in functions. For example, the bellow statements are equivalents
const callback = value => value * 2
const memoizedCb = useCallback(callback, [])
const memoizedWithUseMemo = useMemo(() => callback, [])
So for now on every assertion about useCallback can be applied to useMemo.
The gist of memoization is to keep copies of old values to return in the event we get the same dependencies, this can be great when you have something that is expensive to compute. Take a look at the following code
const Component = ({ items }) =>{
const array = items.map(x => x*2)
}
Uppon every render the const array will be created as a result of a map performed in items. So you can feel tempted to do the following
const Component = ({ items }) =>{
const array = useMemo(() => items.map(x => x*2), [items])
}
Now items.map(x => x*2) will only be executed when items change, but is it worth? The short answer is no. The performance gained by doing this is trivial and sometimes will be more expensive to use memoization than just execute the function each render. Both hooks(useCallback and useMemo) are useful in two distinct use cases:
Referencial equality
When you need to ensure that a reference type will not trigger a re render just for failing a shallow comparison
Computationally expensive operations(only useMemo)
Something like this
const serializedValue = {item: props.item.map(x => ({...x, override: x ? y : z}))}
Now you have a reason to memoized the operation and lazily retrieve the serializedValue everytime props.item changes:
const serializedValue = useMemo(() => ({item: props.item.map(x => ({...x, override: x ? y : z}))}), [props.item])
Any other use case is almost always worth to just re compute all values again, React it's pretty efficient and aditional renders almost never cause performance issues. Keep in mind that sometimes your efforts to optimize your code can go the other way and generate a lot of extra/unecessary code, that won't generate so much benefits (sometimes will only cause more problems).
The List component manages it's own state (list) the delete functions depends on this list being available in it's closure. So when the list changes the delete function must change.
With redux this would not be a problem because deleting items would be accomplished by dispatching an action and will be changed by a reducer that is always the same function.
React happens to have a useReducer hook that you can use:
import React, { useMemo, useReducer, memo } from 'react';
const Item = props => {
//calling remove will dispatch {type:'REMOVE', payload:{id}}
//no arguments are needed
const { remove } = props;
console.log('component render', props);
return (
<div>
<div>{JSON.stringify(props)}</div>
<div>
<button onClick={remove}>REMOVE</button>
</div>
</div>
);
};
//wrap in React.memo so when props don't change
// the ItemContainer will not re render (pure component)
const ItemContainer = memo(props => {
console.log('in the item container');
//dispatch passed by parent use it to dispatch an action
const { dispatch, id } = props;
const remove = () =>
dispatch({
type: 'REMOVE',
payload: { id },
});
return <Item {...props} remove={remove} />;
});
const initialState = [{ id: 1 }, { id: 2 }, { id: 3 }];
//Reducer is static it doesn't need list to be in it's
// scope through closure
const reducer = (state, action) => {
if (action.type === 'REMOVE') {
//remove the id from the list
return state.filter(
item => item.id !== action.payload.id
);
}
return state;
};
export default () => {
//initialize state and reducer
const [list, dispatch] = useReducer(
reducer,
initialState
);
console.log('parent render', list);
return (
<div>
{list.map(({ id }) => (
<ItemContainer
key={id}
id={id}
dispatch={dispatch}
/>
))}
</div>
);
};

Resources