useMemo does not rerender dynamic controls - javascript

I have list of controls which are sent by backend. Sometimes it is necessary to update options of dropdown control.
I thought that it would work. However, memoizedControls is not rerendered.
I believe that it should work like this:
press the button and handleFieldsChange() function is triggered.
then setTechSpec('') sets techSpec to ''
then custom hook usePerson is triggered because it has techSpec in its dependency array
then memoizedControls is triggered because it has personList in its dependency array
and updateControlOptions() updates options of controls
However, UI does not have rerendered and new options of personList is not rerendered.
const [techSpec, setTechSpec] = useState('1')
const [personList, isLoadingPersonList] = usePerson(techSpec)
const handleFieldsChange = (changedValue: EditablePersonValues) => {
setTechSpec('')
fetchPerson()}
const updateControlOptions = (
controls: FormControl[],
controlName: string,
newOptions: SelectOption[],
) =>
controls.map((control) =>
control.name === controlName
? { ...control, options: newOptions }
: { ...control },)
const memoizedControls = useMemo(() => {
console.log('memoizedControls')
if (personList.length > 0)
return updateControlOptions(
controls,
'personId',
personList,
)
return controls
}, [controls, personList])
const fetchPerson = () => {
const localTechSpecification = form.getFieldValue('techSpecification')
setTechSpec(localTechSpecification)
form.setFieldsValue({ personId: undefined })
}
and:
return (
{memoizedControls.map(
({ name, type, displayName, required, options, measure }) => {
return (
<MyDynamicField
key={name}
name={name}
controlType={type}
displayName={`${displayName}${measure ? `, ${measure}` : ''}`}
required={required}
value={value}
itemOptions={options}
/>
)
},
)}
)
My question is that "usePerson" hook is being re-executed when the "techSpec" state value changes. personList is updated. But memoizedControls does not show new values of personList. Maybe do you know the reason of why memoizedControls is not rerendered?
Please, does anybody know what I am doing wrong?

import React, { useState, useMemo } from 'react';
const MyComponent = ({ personList }) => {
const [techSpec, setTechSpec] = useState('1');
const [isLoadingPersonList, setIsLoadingPersonList] = useState(false);
const handleFieldsChange = (changedValue) => {
setTechSpec('');
fetchPerson();
};
const updateControlOptions = (controls, controlName, newOptions) =>
controls.map((control) =>
control.name === controlName
? { ...control, options: newOptions }
: { ...control }
);
const memoizedControls = useMemo(() => {
console.log('memoizedControls');
if (personList.length > 0) {
return updateControlOptions(controls, 'personId', personList);
}
return controls;
}, [controls, personList]); // personList is included in the dependency array
const fetchPerson = () => {
const localTechSpecification = form.getFieldValue('techSpecification');
setTechSpec(localTechSpecification);
form.setFieldsValue({ personId: undefined });
};
return (
<div>
{memoizedControls.map(({ name, type, displayName, required, options, measure }) => (
<DynamicField
key={name}
name={name}
controlType={type}
displayName={`${displayName}${measure ? `, ${measure}` : ''}`}
required={required}
value={value}
itemOptions={options}
/>
))}
</div>
);
};

The above code was fine, the problem was in component <MyDynamicField/>.
So itemOptions should be added in dependency array [height, width, itemOptions]) of useEffect. So code would look like this:
export const MyDynamicField = ({
name,
controlType,
displayName,
required,
value,
itemOptions,
moulds,
initData,
height,
width,
}: Props) => {
const [options, setOptions] = useState<SelectOption[]>([])
useEffect(() => {
const prepareOptions = filterOptions(
name,
moulds,
initData,
itemOptions,
height,
width,
)
setOptions(prepareOptions)
}, [height, width, itemOptions])
return (
<ControlFactory
key={name}
name={name}
controlType={controlType}
displayName={displayName}
required={required}
options={options}
value={value}
/>
)
}
and then useEffect and useState can be removed and useMemo can be used:
export const MyDynamicField = ({
name,
controlType,
displayName,
required,
value,
itemOptions,
moulds,
initData,
height,
width,
}: Props) => {
const options = useMemo(
() => filterOptions(name, moulds, initData, itemOptions, height, width),
[name, moulds, initData, itemOptions, height, width],
)
return (
<ControlFactory
key={name}
name={name}
controlType={controlType}
displayName={displayName}
required={required}
options={options}
value={value}
/>
)
}

Related

Reload table data when select menu item is changed

I have this table with select menu:
export interface IActivePairsProps extends StateProps, DispatchProps, RouteComponentProps<{ url: string }> {}
export const ActivePairs = (props: IActivePairsProps) => {
const [paginationState, setPaginationState] = useState(
overridePaginationStateWithQueryParams(getSortState(props.location, ITEMS_PER_PAGE, 'id'), props.location.search)
);
const [exchangeId, setExchangeId] = useState('');
const getAllEntities = () => {
props.getEntities(paginationState.activePage - 1, paginationState.itemsPerPage, `${paginationState.sort},${paginationState.order}`);
props.getExchangesList();
};
const sortEntities = () => {
getAllEntities();
const endURL = `?page=${paginationState.activePage}&sort=${paginationState.sort},${paginationState.order}&exchangeId=${exchangeId}`;
if (props.location.search !== endURL) {
props.history.push(`${props.location.pathname}${endURL}`);
}
};
useEffect(() => {
sortEntities();
}, [paginationState.activePage, paginationState.order, paginationState.sort]);
useEffect(() => {
const params = new URLSearchParams(props.location.search);
const page = params.get('page');
const sort = params.get('sort');
if (page && sort) {
const sortSplit = sort.split(',');
setPaginationState({
...paginationState,
activePage: +page,
sort: sortSplit[0],
order: sortSplit[1],
});
}
const exchangeId = params.get('exchangeId');
}, [props.location.search]);
const sort = p => () => {
setPaginationState({
...paginationState,
order: paginationState.order === 'asc' ? 'desc' : 'asc',
sort: p,
});
};
const handlePagination = currentPage =>
setPaginationState({
...paginationState,
activePage: currentPage,
});
const handleSyncList = () => {
sortEntities();
};
const { activePairsList, exchangesList, match, loading, totalItems } = props;
return (
<div>
<div className="table-responsive">
{activePairsList && activePairsList.length > 0 ? (
<Table responsive>
<thead>
<tr>
.....
<select onChange={e => setExchangeId(e.target.value)}>
{exchangesList
? exchangesList.map(otherEntity => (
<option value={otherEntity.exchangeId} key={otherEntity.exchangeId}>
{otherEntity.exchangeLongName} - {otherEntity.exchangeId}
</option>
))
: null}
</select>
.........
</Table>
) : (
!loading && <div className="alert alert-warning">No Active Pairs found</div>
)}
</div>
{props.totalItems ? (
<div className={activePairsList && activePairsList.length > 0 ? '' : 'd-none'}>
<Row className="justify-content-center">
<JhiItemCount page={paginationState.activePage} total={totalItems} itemsPerPage={paginationState.itemsPerPage} />
</Row>
<Row className="justify-content-center">
<JhiPagination
activePage={paginationState.activePage}
onSelect={handlePagination}
maxButtons={5}
itemsPerPage={paginationState.itemsPerPage}
totalItems={props.totalItems}
/>
</Row>
</div>
) : (
''
)}
</div>
);
};
const mapStateToProps = ({ activePairs, exchangesList }: IRootState) => ({
activePairsList: activePairs.entities,
exchangesList: exchangesList.entities,
loading: activePairs.loading,
totalItems: activePairs.totalItems,
});
const mapDispatchToProps = {
getEntities,
getExchangesList,
};
type StateProps = ReturnType<typeof mapStateToProps>;
type DispatchProps = typeof mapDispatchToProps;
export default connect(mapStateToProps, mapDispatchToProps)(ActivePairs);
How I can reload the table data when I change the select menu item? I would like to reload the data from the table data with the new selected exchageId param.
useEffect(fn, deps);
As we can see in the React documentation, the way we use the effect hook looks like this:
,fn is the effectful function, and deps is an array of values it depends on. Every time the component renders, React checks if all the values in the deps array are still the same. If any of them has changed since the last render, fn is run again.,All right, so far all the examples exhibit the same behavior. The effect simply doesn't run again if the dependency value doesn't change.
So you only need to give the useEffect hook exchageId as deps and the component's loading function as fn, then UseEffect will rerenders your component.

Handling an indetermined amount of states for n checkboxes in React

I have this solution for handling a fix number of states for a fix number of checkboxes:
import { useState } from 'react';
function App() {
const [arrValues, setArrValues] = useState(
// use a function here to only create the initial array on mount
() => Array.from(
{ length: 10 }
)
);
const setcheckBoxValue = (i) => {
setArrValues(
arrValues.map((v, j) => j !== i ? v : !v)
);
console.log(arrValues);
}
return (
<div className="App">
{arrValues.map( (val, i) =>
<input
key={i}
type="checkbox"
checked={val}
onChange={() => setcheckBoxValue(i)}
>
</input>)
}
</div>
);
}
export default App;
In the code it is 10, but, what if the length of the array is not known until I read some value in a database,
Rafael
You can do something like this. The example below is using an object rather than an array, but this is just example. Also, the “initialValues” could be easily generated from data (from database). These could be the undetermined values you mentioned.
const MyTest = () => {
const initialValues = {
values: { checkbox1: true, checkbox2: false, checkbox3: true }
}
const [state, setState] = useState(initialValues || {})
const handleChange = (event) => {
const target = event.target
const value = target.checked
const name = target.name
setState((prevState) => ({
values: {
...prevState.values,
[name]: value
}
}))
}
return (
<div>
<pre>{JSON.stringify(state, null, 2)}</pre>
{state.values &&
Object.keys(state.values).map((checkboxKey, i) => (
<input
name={checkboxKey}
key={i}
type="checkbox"
checked={state.values[checkboxKey]}
onChange={handleChange}
/>
))}
</div>
)
}

How to get the number of checked checkboxes in React.js?

I started learning React not so long ago. Decided to make some kind of "life checklist" as one of my beginner projects. I have been using Functional Components in the core.
FYI:
I have data.js as an array of objects where "action", "emoji" and unique ID are stored.
I import it into my App.js.
const App = () => {
//Looping over data
const items = data.map((item) => {
return (
<ChecklistItem action={item.action} emoji={item.emoji} key={item.id} />
);
});
return (
<>
<GlobalStyle />
<StyledHeading>Life Checklist</StyledHeading>
<StyledApp>{items}</StyledApp>
<h2>Overall number: {data.length}</h2>
</>
);
};
export default App;
Here is my <ChecklistItem/> component:
const ChecklistItem = ({ action, emoji }) => {
//State
const [isActive, setIsActive] = useState(false);
//Event Handlers
const changeHandler = () => {
setIsActive(!isActive);
};
return (
<StyledChecklistItem isActive={isActive}>
<input type="checkbox" checked={isActive} onChange={changeHandler} />
<StyledEmoji role="img">{emoji}</StyledEmoji>
<StyledCaption>{action}</StyledCaption>
</StyledChecklistItem>
);
};
export default ChecklistItem;
I would be satisfied with the functionality so far, but I need to show how many "active" checklist items were chosen in the parent <App/> component like "You have chosen X items out of {data.length}. How can I achieve this?
I assume that I need to lift the state up, but cannot understand how to implement this properly yet.
You can do that by simply creating a state for storing this particular count of active items.
To do that, you would need to update your <App/> component to something like this
const App = () => {
const [activeItemsCount, setActiveItemsCount] = useState(0);
//Looping over data
const items = data.map((item, index) => {
return (
<ChecklistItem
key={index}
action={item.action}
emoji={item.emoji}
setActiveItemsCount={setActiveItemsCount}
/>
);
});
return (
<>
<h1>Life Checklist</h1>
<div>{items}</div>
<div>Active {activeItemsCount} </div>
<h2>Overall number: {data.length}</h2>
</>
);
};
export default App;
And then in your <ChecklistItem /> component, you would need to accept that setActiveItemsCount function so that you can change the state of the activeItemsCount.
import React, { useState, useEffect } from "react";
const ChecklistItem = ({ action, emoji, setActiveItemsCount }) => {
const [isActive, setIsActive] = useState(false);
const changeHandler = () => {
setIsActive(!isActive);
};
useEffect(() => {
if (!isActive) {
setActiveItemsCount((prevCount) => {
if (prevCount !== 0) {
return prevCount - 1;
}
return prevCount;
});
}
if (isActive) {
setActiveItemsCount((prevCount) => prevCount + 1);
}
}, [isActive, setActiveItemsCount]);
return <input type="checkbox" checked={isActive} onChange={changeHandler} />;
};
export default ChecklistItem;
By using the useEffect and the checks for isActive and 0 value, you can nicely increment or decrement the active count number by pressing the checkboxes.
How about this?
const data = [
{ action: '1', emoji: '1', id: 1 },
{ action: '2', emoji: '2', id: 2 },
{ action: '3', emoji: '3', id: 3 },
];
const ChecklistItem = ({ action, emoji, isActive, changeHandler }) => {
return (
<div isActive={isActive}>
<input type="checkbox" checked={isActive} onChange={changeHandler} />
<div>{emoji}</div>
<div>{action}</div>
</div>
);
};
const PageContainer = () => {
const [checkedItemIds, setCheckedItemIds] = useState([]);
function changeHandler(itemId) {
if (checkedItemIds.indexOf(itemId) > -1) {
setCheckedItemIds((prev) => prev.filter((i) => i !== itemId));
} else {
setCheckedItemIds((prev) => [...prev, itemId]);
}
}
const items = data.map((item) => {
const isActive = checkedItemIds.indexOf(item.id) > -1;
return (
<ChecklistItem
isActive={isActive}
changeHandler={() => changeHandler(item.id)}
action={item.action}
emoji={item.emoji}
key={item.id}
/>
);
});
return (
<div className="bg-gray-100">
<div>{items}</div>
<h2>
You have chosen {checkedItemIds.length} items out of {data.length}
</h2>
</div>
);
};
When data is used by a child component, but the parent needs to be aware of it for various reasons, that should be state in the parent component. That state is then handed to the child as props.
One way to do this would be to initialize your parent component with a piece of state that was an array of boolean values all initialized to false. Map that state into the checkbox components themselves and hand isActive as a prop based on that boolean value. You should then also hand the children a function of the parent that will change the state of the boolean value at a certain index of that array.
Here's a bit of a contrived example:
// Parent.tsx
const [checkBoxes, setCheckboxes] = useState(data.map(data => ({
id: data.id,
action: data.action,
emoji: data.emoji
isActive: false,
})));
const handleCheckedChange = (i) => {
setCheckboxes(checkBoxes => {
checkBoxes[i].isActive = !checkBoxes[i].isActive;
return checkBoxes;
})
}
return(
checkBoxes.map((item, i) =>
<ChecklistItem
action={item.action}
emoji={item.emoji}
key={item.id}
index={i}
isActive={item.isActive}
handleChange={handleCheckedChange}
/>
)
);
// CheckListItem.tsx
const CheckListItem = ({ action, emoji, index, isActive, handleChange }) => (
<StyledChecklistItem isActive={isActive}>
<input type="checkbox" checked={isActive} onChange={() => handleChange(index)} />
<StyledEmoji role="img">{emoji}</StyledEmoji>
<StyledCaption>{action}</StyledCaption>
</StyledChecklistItem>
)

Couldn't correctly initialize state in parent component from children states

I have two React components, namely, Form and SimpleCheckbox.
SimpleCheckbox uses some of the Material UI components but I believe they are irrelevant to my question.
In the Form, useEffect calls api.getCategoryNames() which resolves to an array of categories, e.g, ['Information', 'Investigation', 'Transaction', 'Pain'].
My goal is to access checkboxes' states(checked or not) in the parent component(Form). I have taken the approach suggested in this question.(See the verified answer)
Interestingly, when I log the checks it gives(after api call resolves):
{Pain: false}
What I expect is:
{
Information: false,
Investigation: false,
Transaction: false,
Pain: false,
}
Further More, checks state updates correctly when I click into checkboxes. For example, let's say I have checked Information and Investigation boxes, check becomes the following:
{
Pain: false,
Information: true,
Investigation: true,
}
Here is the components:
const Form = () => {
const [checks, setChecks] = useState({});
const [categories, setCategories] = useState([]);
const handleCheckChange = (isChecked, category) => {
setChecks({ ...checks, [category]: isChecked });
}
useEffect(() => {
api
.getCategoryNames()
.then((_categories) => {
setCategories(_categories);
})
.catch((error) => {
console.log(error);
});
}, []);
return (
{categories.map(category => {
<SimpleCheckbox
label={category}
onCheck={handleCheckChange}
key={category}
id={category}
/>
}
)
}
const SimpleCheckbox = ({ onCheck, label, id }) => {
const [check, setCheck] = useState(false);
const handleChange = (event) => {
setCheck(event.target.checked);
};
useEffect(() => {
onCheck(check, id);
}, [check]);
return (
<FormControl>
<FormControlLabel
control={
<Checkbox checked={check} onChange={handleChange} color="primary" />
}
label={label}
/>
</FormControl>
);
}
What I was missing was using functional updates in setChecks. Hooks API Reference says that: If the new state is computed using the previous state, you can pass a function to setState.
So after changing:
const handleCheckChange = (isChecked, category) => {
setChecks({ ...checks, [category]: isChecked });
}
to
const handleCheckChange = (isChecked, category) => {
setChecks(prevChecks => { ...prevChecks, [category]: isChecked });
}
It has started to work as I expected.
It looks like you're controlling state twice, at the form level and at the checkbox component level.
I eliminated one of those states and change handlers. In addition, I set checks to have an initialState so that you don't get an uncontrolled to controlled input warning
import React, { useState, useEffect } from "react";
import { FormControl, FormControlLabel, Checkbox } from "#material-ui/core";
import "./styles.css";
export default function App() {
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<Form />
</div>
);
}
const Form = () => {
const [checks, setChecks] = useState({
Information: false,
Investigation: false,
Transaction: false,
Pain: false
});
const [categories, setCategories] = useState([]);
console.log("checks", checks);
console.log("categories", categories);
const handleCheckChange = (isChecked, category) => {
setChecks({ ...checks, [category]: isChecked });
};
useEffect(() => {
// api
// .getCategoryNames()
// .then(_categories => {
// setCategories(_categories);
// })
// .catch(error => {
// console.log(error);
// });
setCategories(["Information", "Investigation", "Transaction", "Pain"]);
}, []);
return (
<>
{categories.map(category => (
<SimpleCheckbox
label={category}
onCheck={handleCheckChange}
key={category}
id={category}
check={checks[category]}
/>
))}
</>
);
};
const SimpleCheckbox = ({ onCheck, label, check }) => {
return (
<FormControl>
<FormControlLabel
control={
<Checkbox
checked={check}
onChange={() => onCheck(!check, label)}
color="primary"
/>
}
label={label}
/>
</FormControl>
);
};
If you expect checks to by dynamically served by an api you can write a fetchHandler that awaits the results of the api and updates both slices of state
const fetchChecks = async () => {
let categoriesFromAPI = ["Information", "Investigation", "Transaction", "Pain"] // api result needs await
setCategories(categoriesFromAPI);
let initialChecks = categoriesFromAPI.reduce((acc, cur) => {
acc[cur] = false
return acc
}, {})
setChecks(initialChecks)
}
useEffect(() => {
fetchChecks()
}, []);
I hardcoded the categoriesFromApi variable, make sure you add await in front of your api call statement.
let categoriesFromApi = await axios.get(url)
Lastly, set your initial slice of state to an empty object
const [checks, setChecks] = useState({});

React hook callback from child to parent

I have this child component called TodoList
const TodoItem = ({ checked, children }) =>
(<TouchableOpacity
style={{ backgroundColor: checked && 'red'}}>
{children}
</TouchableOpacity>
);
const TodoList = props => {
const {
options = [],
onSelect,
...rest
} = props;
const [selectedOptionIndex, setSelectedOptionIndex] = useState(null);
useEffect(() => {
onSelect(options[selectedOptionIndex]);
}, [onSelect, options, selectedOptionIndex]);
const renderItem = (o, index) => {
return (
<TodoItem
key={o + index}
onPress={() => setSelectedOptionIndex(index)}
checked={index === selectedOptionIndex}>
{index === selectedOptionIndex && <Tick />}
<Text>{o}</Text>
</TodoItem>
);
};
return (
<View {...rest}>{options.map(renderItem)}</View>
);
};
export default TodoList;
And I have a parent component called Container
export default function() {
const [item, setItem] = setState(null);
return (
<Screen>
<TodoList options={[1,2,3]} onSelect={(i) => setItem(i)} />
</Screen>
)
}
I want to have a callback from child component to parent component using onSelect whenever a TodoItem is selected. However, whenever the onSelect is called, my TodoList re-renders and my selectedOptionIndex is reset. Hence, my checked flag will only change to true briefly before resetting to false.
If I remove the onSelect callback, it works fine. But I need to setState for both child and parent. How do I do that?
It's hard to tell why thats happening for you, most likely because the container's state is changing, causing everything to rerender.
Something like this should help you out, though.
const { render } = ReactDOM;
const { useEffect, useState } = React;
const ToDoItem = ({checked, label, onChange, style}) => {
const handleChange = event => onChange(event);
return (
<div style={style}>
<input type="checkbox" checked={checked} onChange={handleChange}/>
{label}
</div>
);
}
const ToDoList = ({items, onChosen}) => {
const [selected, setSelected] = useState([]);
const handleChange = item => event => {
let s = [...selected];
s.includes(item) ? s.splice(s.indexOf(item), 1) : s.push(item);
setSelected(s);
onChosen(s);
}
return (
<div>
{items && items.map(i => {
let s = selected.includes(i);
return (
<ToDoItem
key={i}
label={i}
onChange={handleChange(i)}
checked={s}
style={{textDecoration: s ? 'line-through' : ''}}/>
)
})}
</div>
);
}
const App = () => {
const [chosen, setChosen] = useState();
const handleChosen = choices => {
setChosen(choices);
}
return (
<div>
<ToDoList items={["Rock", "Paper", "Scissors"]} onChosen={handleChosen} />
{chosen && chosen.length > 0 && <pre>Chosen: {JSON.stringify(chosen,null,2)}</pre>}
</div>
);
}
render(<App />, document.body)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.9.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.9.0/umd/react-dom.production.min.js"></script>
Turned out my top-level component Screen is causing this re-render. In my Screen functional component, I have this piece of code before the return
const Content = scroll
? contentProps => {
const { style: contentContainerStyle } = contentProps;
return (
<ScrollView {...contentContainerStyle}>
{contentProps.children}
</ScrollView>
);
}
: View;
return (
<Content>{children}</Content>
)
And it somehow (not sure why) causes the children to re-render every time my state changes.
I fixed it by removing the function and have it simply returning a View
const Content = scroll ? ScrollView : View;

Categories