React memoize inside a map function - reactjs

Here is my problem: I'm making in Ract a clone of Google Forms. The structure of the data in the Redux store is as follows:
sections: [
{
sectionTitle: 'first section',
questions: [
{
questionType: 'checkbox',
question: 'some question',
options: [
{
number: 1,
text: 'sometimes',
},
],
},
],
},
],
In the feature loading the main edig page, I render this questions in the following map:
{form.sections.map((section, index) => {
return (
<Box key={index}>
<Box mb={5}>
<SectionCard index={index} />
</Box>
{section.questions.map((question, questionIndex) => {
return (
<QuestionCard
key={questionIndex}
sectionIndex={index}
questionIndex={questionIndex}
/>
);
})}
</Box>
);
})}
Each component picks the values from the store with useSelector, like the question description, the options, question type.
QuestionCard:
const QuestionCard = ({ sectionIndex, questionIndex }) => {
const question = useSelector(
state => state.form.sections[sectionIndex].questions[questionIndex]
);
const [hasQuestionChanged, setHasQuestionChanged] = useState(false);
const dispatch = useDispatch();
const handleChangeQuestionValue = e => {
dispatch(
setQuestionDescription({
sectionNumber: sectionIndex,
questionNumber: questionIndex,
newValue: e.target.value,
})
);
setHasQuestionChanged(true);
};
return(
...
<CardContent>
<TextField
multiline
label={`Question ${questionIndex + 1}`}
value={question.question}
margin='normal'
variant='standard'
onChange={handleChangeQuestionValue}
fullWidth
/>
{question.questionType === 'multiple' &&
question.options.map((option, index) => {
return (
<RadioQuestion
key={index}
sectionIndex={sectionIndex}
questionIndex={questionIndex}
optionIndex={index}
/>
);
})}
...
)
RadioQuestion
const RadioQuestion = memo(({ sectionIndex, questionIndex, optionIndex }) => {
const theme = useTheme();
const dispatch = useDispatch();
const [selected, setSelected] = useState(false);
const option = useSelector(
state =>
state.form.sections[sectionIndex].questions[questionIndex].options[
optionIndex
]
);
const handleChangeOptionDescription = e => {
dispatch(
setQuestionOptionDescription({
sectionIndex: sectionIndex,
questionIndex: questionIndex,
optionIndex: optionIndex,
description: e.target.value,
})
);
// Update the state
const handleChangeOptionValue = e => {
setSelected(!selected);
};
return(
...
<FormControl mt='1rem'>
<RadioGroup>
<Box justifyContent='center' sx={{ alignItems: 'center' }}>
<FormControlLabel
labelPlacement='top'
value={option.number}
control={<Radio />}
checked={selected}
sx={{ mt: 3 }}
onClick={handleChangeOptionValue}
/>
<FormControl>
<TextField
fullWidth
label={`Option ${option.number}`}
value={option.text ? option.text : ''}
variant='standard'
margin='normal'
onChange={handleChangeOptionDescription}
/>
</FormControl>
</Box>
</RadioGroup>
</FormControl>
...
)
SectionCard
const SectionCard = ({ index }) => {
const isMobile = useMediaQuery('(max-width: 400px)');
const theme = useTheme();
const dispatch = useDispatch();
const [errorTitle, setErrorTitle] = useState();
const { sectionTitle } = useSelector(state => state.form.sections[index]);
const totalSections = useSelector(
state => state.form.computedSections.computed.totalSections
);
const handleChangeSectionTitle = e => {
dispatch(
changeSectionTitle({ sectionIndex: index, newValue: e.target.value })
);
};
return (
...
<FormControl>
<TextField
required
autoComplete='off'
sx={{ minWidth: isMobile ? 150 : 400 }}
label='Section Title'
variant='standard'
value={sectionTitle}
margin='normal'
color={theme.palette.secondary[100]}
onChange={handleChangeSectionTitle}
error={errorTitle}
/>
{errorTitle && (
<FormHelperText error>
The Section Title can't be empty
</FormHelperText>
)}
</FormControl>
...
)
The problem is that when I edit a question filed and dispatch the value it rerenders all the question cards, which makes typing a bit slow. If I try to memoize the QuestionCard or the SectionCard I get a TypeError:
const SectionCard = memo(({ index }) => {...})
>Uncaught TypeError: Component is not a function
How the component looks
As you can see, every time I change a value, 20 rerenders happen, so I need to only rerender que component that has changed and I can't find the way to do it. I believe that the root problem is that the state is mapping through the sections and questions, and returning a new state, making all the components rerender.
As another guess on the problem and its solution, I understand that I shoul use useSelector to memoize, but I am really lost since the data structured is so nested.
Thanks in advance
I tried memoizing the function that renders the Section and the Question, which gives an error, I tried to useMemo on the parent component ant render the memoized component instead of the original. I am not sure on how to handle to render only when that specific component has changed.

Related

useEffect doesn't re-render on state change or infinite looping issue

I have a component which contains a form and a list. When user adds an item to the list though the form, the item should display immediately in the list. I try to use useEffect to fetch data, useEffect without dependency causes an infinite request loop. I added empty array as dependency to prevent looping but in this case new item which is added doesn't display in the list until refreshing the page. How can I solve this issue? (I use antd and antd-form-builder to create the component)
here is my code:
function FieldSetting() {
const [form] = Form.useForm()
const [typeValue, setTypeValue] = useState()
const meta = {
fields: [{ key: "pathname", onChange: (e) => setTypeValue(e.target.value) }],
}
const [data, setData] = useState([])
async function onFinish() {
try {
await axios.post("api", { typeValue, typeId })
form.resetFields()
} catch (e) {
console.log(e)
}
}
useEffect(() => {
const getData = async () => {
const response = await fetch(`api?id=${typeId}`)
const newData = await response.json()
setData(newData)
}
getData()
}, [])
return (
<Container>
<Form form={form} layout="inline" className="form-field" onFinish={onFinish}>
<FormBuilder form={form} meta={meta} />
<Form.Item>
<Button type="primary" htmlType="submit">
Add
</Button>
</Form.Item>
</Form>
<div
id="scrollableDiv"
style={{
height: 665,
overflow: "auto",
padding: "0 16px",
border: "1px solid rgba(140, 140, 140, 0.35)",
}}
>
<List
itemLayout="horizontal"
dataSource={data}
renderItem={(item) => (
<List.Item
actions={[
<a key="list-edit">edit</a>,
<a onClick={() => axios.delete(`http://gage.axaneh.com/api/Gages/SettingProduct/RemoveProductSetting/${item.id}`, item)} key="list-delete">
delete
</a>,
]}
>
<List.Item.Meta title={item.typeValue} />
</List.Item>
)}
/>
</div>
</Container>
)
}
export default FieldSetting
Just add a state that will refretch (trigger useEffect) after you have submitted the form. Be aware that it will refetch all the data from the API. This might bring scalability issues when the data grows.
function FieldSetting() {
const [form] = Form.useForm()
const [refetch, setRefetch] = useState(false) // <----- add this state
const [typeValue, setTypeValue] = useState()
const meta = {
fields: [{ key: "pathname", onChange: (e) => setTypeValue(e.target.value) }],
}
const [data, setData] = useState([])
async function onFinish() {
try {
await axios.post("api", { typeValue, typeId })
form.resetFields()
setRefetch(!refetch) // <----- set the refetch to change the state
} catch (e) {
console.log(e)
}
}
useEffect(() => {
const getData = async () => {
const response = await fetch(`api?id=${typeId}`)
const newData = await response.json()
setData(newData)
}
getData()
}, [refetch]) // <----- add the refetch here to trigger the effect
return (
<Container>
<Form form={form} layout="inline" className="form-field" onFinish={onFinish}>
<FormBuilder form={form} meta={meta}
/>
<Form.Item>
<Button type="primary" htmlType="submit">
Add
</Button>
</Form.Item>
</Form>
<div
id="scrollableDiv"
style={{
height: 665,
overflow: "auto",
padding: "0 16px",
border: "1px solid rgba(140, 140, 140, 0.35)",
}}
>
<List
itemLayout="horizontal"
dataSource={data}
renderItem={(item) => (
<List.Item
actions={[
<a key="list-edit">edit</a>,
<a onClick={() => axios.delete(`http://gage.axaneh.com/api/Gages/SettingProduct/RemoveProductSetting/${item.id}`, item)} key="list-delete">
delete
</a>,
]}
>
<List.Item.Meta title={item.typeValue} />
</List.Item>
)}
/>
</div>
</Container>
)
}
export default FieldSetting```
Whenever you manipulate your array just add a dummy state and change it
add this state
const [extra, setExtra] = useState(0)
when you change the state of your array like add or remove just add this line below
setExtra(extra+1)
what happens is that adding or removing data in an array don't count as a state change in react as per my understanding it need to be something different like true to false or in this case 0 to 1

I can't update state when submitted in a form

I need to update the state on main page, the problem is that I update values 3 levels down...
This component is where I get all the cities, putting the data on the State and map through cities.
CitiesPage.tsx
export const CitiesPage = () => {
const [cities, setCities] = useState<City[]>([]);
useEffect(() => {
getCities().then(setCities);
}, []);
return (
<>
<PageTitle title="Cities" />
<StyledCitySection>
<div className="headings">
<p>Name</p>
<p>Iso Code</p>
<p>Country</p>
<span style={{ width: "50px" }}></span>
</div>
<div className="cities">
{cities.map((city) => {
return <CityCard key={city.id} city={city} />;
})}
</div>
</StyledCitySection>
</>
);
};
On the next component, I show cities and options to show modals for delete and update.
CityCard.tsx.
export const CityCard = ({ city }: CityProps) => {
const [showModal, setShowModal] = useState(false);
const handleModal = () => {
setShowModal(true);
};
const handleClose = () => {
setShowModal(false);
};
return (
<>
{showModal && (
<ModalPortal onClose={handleClose}>
<EditCityForm city={city} closeModal={setShowModal} />
</ModalPortal>
)}
<StyledCityCard>
<p className="name">{city.name}</p>
<p className="isoCode">{city.isoCode}</p>
<p className="country">{city.country?.name}</p>
<div className="options">
<span className="edit">
<FiEdit size={18} onClick={handleModal} />
</span>
<span className="delete">
<AiOutlineDelete size={20} />
</span>
</div>
</StyledCityCard>
</>
);
};
and finally, third levels down, I have this component.
EditCityForm.tsx.
export const EditCityForm = ({ city, closeModal }: Props) => {
const [updateCity, setUpdateCity] = useState<UpdateCity>({
countryId: "",
isoCode: "",
name: "",
});
const handleChange = (
evt: ChangeEvent<HTMLInputElement | HTMLSelectElement>
) => {
const { target } = evt;
setUpdateCity({ ...updateCity, [target.name]: target.value });
};
const handleSubmit = (evt: FormEvent<HTMLFormElement>) => {
evt.preventDefault();
const { id: cityId } = city;
updateCityHelper(cityId, updateCity);
closeModal(false);
};
useEffect(() => {
setUpdateCity({
isoCode: city.isoCode,
name: city.name,
countryId: city.country?.id,
});
}, [city]);
return (
<form onSubmit={handleSubmit}>
<Input
label="Iso Code"
type="text"
placeholder="Type a IsoCode..."
onChange={handleChange}
name="isoCode"
value={updateCity.isoCode}
/>
<Input
label="Name"
type="text"
placeholder="Paste a Name.."
onChange={handleChange}
name="name"
value={updateCity.name}
/>
<CountrySelect
label="Country"
onChange={handleChange}
value={city.country?.name || ""}
name="countryId"
/>
<Button type="submit" color="green" text="Update" />
</form>
);
};
Edit form where retrieve data passed from CityCard.tsx and update State, passing data to a function that update Info, closed modal and... this is where I don't know what to do.
How can I show the info updated on CitiesPage.tsx when I submitted on EditCityForm.tsx
Any help will be appreciated.
Thanks!
You are storing the updated value in a different state, namely the updateCity state, but what you should be doing is update the origional cities state. While these two states are not related, and at the same time your UI is depend on cities state's data, so if you wish to update UI, what you need to do is update cities' state by using it's setter function setCities.
Just like passing down state, you pass it's setters as well, and use the setter function to update state's value:
// CitiesPage
{cities.map((city) => {
return <CityCard key={city.id} city={city} setCities={setCities} />;
})}
// CityCard
export const CityCard = ({ city, setCities }: CityProps) => {
// ...
return (
// ...
<ModalPortal onClose={handleClose}>
<EditCityForm city={city} closeModal={setShowModal} setCities={setCities} />
</ModalPortal>
// ...
)
}
// EditCityForm
export const EditCityForm = ({ city, closeModal, setCities }: Props) => {
// const [updateCity, setUpdateCity] = useState<UpdateCity>({ // no need for this
// countryId: "",
// isoCode: "",
// name: "",
// });
const handleSubmit = (evt: FormEvent<HTMLFormElement>) => {
evt.preventDefault();
const { id: cityId } = city;
setCities(); // your update logic
closeModal(false);
};
}
I'd advise you to use React-Redux or Context API whenever you have nested structures and want to access data throughout your app.
However in this case you can pass setCities and cities as a prop to CityCard and then pass this same prop in the EditCityForm component and you can do something like this in your handleSubmit.
const handleSubmit = (evt: FormEvent<HTMLFormElement>) => {
evt.preventDefault();
let updatedCities = [...cities];
updatedCities.forEach(el => {
if(el.id == updateCity.id) {
el.countryCode = updateCity.countryCode;
el.name = updateCity.name;
el.isoCode = updateCity.isoCode;
}
})
setCities(updatedCities);
closeModal(false);
};

ReactJS Redux toolkit Component does not update, only after refreshing the page

Im using Redux-toolkit (already using it for a while). When I reset my redux store, and load my website for the first time the SharedList component does not update if I update the store.
But the strange thing... after refreshing the page, everything works perfect!
Im using the [...array] and Object.assign({}) methods. Also console logged, and the objects are NOT the same.
this is the reducer: (Only updates the ReactJS component after I refresh my page)
CREATE_SHARED_LIST_ITEM_LOCAL: (
proxyState,
action: PayloadAction<{
active: boolean;
checked: boolean;
id: string;
text: string;
}>
) => {
const state = current(proxyState);
const [groupIndex, group] = getCurrentGroupIndex(state);
const { id, text, active, checked } = action.payload;
const listItemIndex = group.shared_list.findIndex((item) => {
return item.id === id;
});
const group_id =
proxyState.groups[groupIndex].shared_list[listItemIndex].group_id;
const sharedItem: SharedListItem = {
text,
id,
checked,
active,
group_id,
};
proxyState.groups[groupIndex].shared_list[listItemIndex] = Object.assign(
{},
sharedItem
);
},
This is the ReactJS component
const GroceryList: React.FC = () => {
const [update, setUpdate] = useState(false);
const handleUpdate = () => {};
const dispatch = useDispatch();
const listItems = useSelector(selectSharedList);
const { setTitle } = useContext(HeaderContext);
const editItem = async (
id: string,
checked: boolean,
text: string,
active: boolean
) => {
dispatch(MUTATE_SHARED_LIST_LOCAL({ id, text, active, checked }));
};
const fetchList = async () => {
await dispatch(QUERY_SHARED_LIST({}));
};
useEffect(() => {
setTitle("Lijst");
fetchList();
}, []);
const list = listItems.filter((item) => {
return item.active;
});
return (
<>
<Card
// style={{
// paddingBottom: 56,
// }}
>
<CardContent>
<List>
{list.length === 0 ? (
<Box textAlign="center" justifyContent="center">
{/* <Center> */}
<Box>
<EmptyCartIcon style={{ width: "45%" }} />
</Box>
{/* </Center> */}
<Typography>De gedeelte lijst is leeg</Typography>
</Box>
) : null}
{list.map((item, index) => {
const { checked, text, id, active } = item;
return (
<ListItem dense disablePadding key={"item-" + index}>
<Checkbox
checked={checked}
onClick={() => {
editItem(id, !checked, text, active);
}}
/>
<EditTextSharedListItem item={item} />
<Box>
<IconButton
onClick={() => {
editItem(id, checked, text, false);
}}
>
<DeleteOutlineRoundedIcon />
</IconButton>
</Box>
</ListItem>
);
})}
</List>
</CardContent>
</Card>
<AddSharedListItem />
</>
);
};
This is the selector
export const selectSharedList = (state: RootState) => {
const group = selectCurrentGroup(state);
return group.shared_list.filter((item) => {
return item.active;
});
};

Create custom Search bar in react to search through Firebase document

I want to create a custom search bar to query my Firestore document retrieve collection based on user input.
I know there are better options to do this like Algolia, Typesense etc.
But I have issues with Firebase upgrading my account, and I have contacted the Firebase team.
DrinkSearch.tsx
const DrinkSearch: React.FC = () => {
const [searchTerm, setSearchTerm] = useState("");
const [drinkSnap, setDrinkSnap] = useState<
QueryDocumentSnapshot<DocumentData>[]
>([]);
const [isLoading, setIsLoading] = useState(false);
const drinkRef = collection(firebaseFirestore, "products");
const drinkQuery = query(drinkRef, where("drinkName", "==", searchTerm));
const snapshots = getDocs(drinkQuery);
let docsIsEmpty!: boolean;
const getProductOnChange = () => {
setIsLoading(true);
snapshots
.then((docsSnapshot) => {
setIsLoading(false);
setDrinkSnap(docsSnapshot?.docs);
docsIsEmpty = docsSnapshot?.empty;
console.log(docsSnapshot?.docs);
})
.catch((error: FirestoreError) => {
setIsLoading(false);
console.log(error.message);
});
};
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setSearchTerm(e.currentTarget.value);
getProductOnChange();
};
useEffect(() => {
console.log(searchTerm);
}, [searchTerm]);
return (
<Box>
<InputGroup size="lg">
<InputLeftElement pointerEvents="none">
<RiSearch2Line color="#CBD5E0" size="20px" />
</InputLeftElement>
<Input
onChange={handleChange}
type="text"
_focus={{
boxShadow: shadowSm,
}}
fontSize="14px"
placeholder="Search for drinks"
/>
</InputGroup>
<Box
padding={5}
bgColor="white"
height="40px"
borderBottomRadius={"8px"}
border={"1px solid #EDF2F7"}
>
{docsIsEmpty && <Text>Drink not found.</Text>}
{isLoading && (
<Flex height="100%">
<Spinner size={"sm"} colorScheme={"primary.500"} />
</Flex>
)}
{drinkSnap &&
drinkSnap?.map((drinkSnap) => {
const drinks = drinkSnap?.data();
return (
<HStack
cursor={"pointer"}
justify={"space-between"}
padding={"5px"}
_hover={{
bgColor: "#EDF2F7",
}}
key={drinkSnap?.id}
>
<Text fontWeight={"semibold"}>{drinks?.drinkName}</Text>
<Badge fontSize={"12px"}>{drinks?.category}</Badge>
</HStack>
);
})}
</Box>
</Box>
);
};
export default DrinkSearch;
Result: When I start typing for example black label is the name of a drink, nothing happens i.e the [] is empty. When I remove 'l'. it remains black labe, it returns the array with the collection.
What I want: On typing, return all collections that match what is typed.

Facing issue while using composition with a parent and a base component for react.js/typescript

What i'm trying to achieve is to have a BaseComponent which will be reuse in different ParentComponent.
My base card component props is ->
export type MSGameCardProps = {
title: string;
fetchGamesFn : (searchText: string) => Promise<IResultObject<any,any>>;
};
My base card render all the necessary basic logics and controls (inputs,autocomplete,title).
For example, it provide an autocomplete which have a simple search debounce functionality.
Me parent component will not necessary have props and will use the base card like so :
export type MSGameSrcCardProps = {};
export const MSGameSrcCard: React.FC<MSGameSrcCardProps> = () => {
const gameSvc = useGameService();
const fetchGame = async (searchText: string) => {
const rs = await result(gameSvc.getAll(searchText));
return rs;
};
return (
<MSGameCard title={"Convert From"} fetchGamesFn={fetchGame}></MSGameCard>
);
};
export default MSGameSrcCard;
The parent component will provide a fetchGames function which can be different.
It will also set the title and may later on set some other flags.
This pattern result with this error : Type '{}' is missing the following properties from type 'MSGameCardProps': title, fetchGamesFn when trying to use the parent component in my page like so : <MSGameSrcCard></MSGameSrcCard>
I don't understand why my parent should have those properties since they are only required in the child component and are fullfill in my parent component function.
I don't want to make them optional(?) since they are actually required; of course only for my base component
I did try to export my basecomponent AS ANY which remove the error but now my props.fetchGamesFn is always undefined even passing it in inside my parent component function.
Maybe i'm doing it wrong but is there a way to have a parent components with no props with child that required props?
EDIT : Here is my MSGameCard base component definition
export const MSGameCard: React.FC<MSGameCardProps> = props => {
const [games, setGames] = React.useState([
{
name: ""
}
]);
const [selectedGame, setSelectedGame] = React.useState<any>();
const [previousGame, setPreviousGame] = React.useState<any>();
const [isLoading, setIsLoading] = React.useState(false);
const [opacity, setOpacity] = React.useState(0);
const fetchGameBase = (searchText: string) => {
setIsLoading(true);
console.log(props.fetchGamesFn);
props.fetchGamesFn(searchText).then(rs =>{
if (rs.isSuccess) setGames(rs.result.data);
setIsLoading(false);
})
};
const searchDebounce = debounce(300, fetchGameBase);
React.useEffect(() => {
fetchGameBase("");
}, []);
const onGameChanged = (event: any, value: any) => {
if (selectedGame) setPreviousGame(selectedGame);
setOpacity(0);
if (value) {
setTimeout(() => {
setSelectedGame(value);
setOpacity(0.2);
}, 300);
}
};
const onInputChanged = (e: any) => {
let value = e.target.value;
if (!value) value = "";
searchDebounce(value);
};
const getSelectedGameImg = () => {
const bgUrl: string = selectedGame
? selectedGame.bg_url
: previousGame?.bg_url;
return bgUrl;
};
return (
<Card style={{ position: "relative", zIndex: 1 }} variant="outlined">
<CardContent style={{ zIndex: 1 }}>
<Typography variant="h5" gutterBottom>
{props.title}
</Typography>
<Grid container spacing={3}>
<Grid item xs={12} sm={6}>
<Autocomplete
options={games}
getOptionLabel={option => option.name}
onChange={onGameChanged}
onInputChange={onInputChanged}
renderInput={params => (
<TextField
{...params}
label="Source game"
fullWidth
InputProps={{
...params.InputProps,
endAdornment: (
<React.Fragment>
{isLoading ? (
<CircularProgress color="primary" size={30} />
) : null}
{params.InputProps.endAdornment}
</React.Fragment>
)
}}
/>
)}
/>
</Grid>
</Grid>
<Grid container direction="row" spacing={3}>
<Grid item xs={12} sm={6}>
<TextField label="DPI" fullWidth />
</Grid>
<Grid item xs={12} sm={6}>
<TextField label="Sensitivity" fullWidth />
</Grid>
</Grid>
</CardContent>
<img
style={{ opacity: opacity }}
className="gameImage"
src={getSelectedGameImg()}
/>
</Card>
);
};
export default MSGameCard;
Keep updating notice
After checked the minimum reproducible example you have provided.
I found no type error, am I missing something?
Since the error occurred in both two props, I would leave only the string for the check
import * as React from "react";
import "./styles.css";
export type MSGameCardProps = {
title: string;
};
export type MSGameSrcCardProps = {};
export const MSGameSrcCard: React.SFC<MSGameSrcCardProps> = () => {
return <MSGameCard title={"Convert From"} />;
};
const MSGameCard: React.SFC<MSGameCardProps> = (props: MSGameCardProps) => {
console.log(props); // Object {title: "Convert From"}
return <></>;
};
export default function App() {
return (
<div className="App">
<MSGameSrcCard />
</div>
);
}
Try it online here:

Resources