Unexpected result with React Hooks setState - javascript

I am getting some unexpected results.
Looking at that
import React from 'react';
import PropTypes from 'prop-types';
const propTypes = {
fooList: PropTypes.array
};
const defaultProps = {
fooList: [
{ active: false },
{ active: false }
];
};
const FooBar = ({
fooList
}) => {
const [state, setState] = React.useState(fooList);
const onClick = (entry, index) => {
entry.active = !entry.active;
state[index] = entry;
console.log('#1', state) // <- That loggs the new State properly!
setState(state);
}
console.log('#2', state) // <- That does not log at after clicking on the text, only after the initial render
return state.map((entry, index) => {
return <p
onClick={() => onClick(entry, index)}
key={index}>
{`Entry is: ${entry.active ? 'active' : 'not active'}`}
</p>
})
}
FooBar.defaultProps = defaultProps;
FooBar.propTypes = propTypes;
export default FooBar;
I expect on every click the text in the <p /> Tag to change from Entry is: not active to Entry is: active.
Now, I am not sure if I can simply alter the state like this
state[index] = entry;
Using a class extending React.Component, this wouldn't work. But maybe with React Hooks? And then, I am not sure if I can use hooks in a map().

When you use state[index] = entry;, you are mutating the state but the state reference does not change, and so React will not be able to tell if the state changed, and will not re-render.
You can copy the state before mutating it:
const onClick = (entry, index) => {
entry.active = !entry.active;
const newState = [...state];
newState[index] = entry;
console.log(newState) // <- That loggs the new State properly!
setState(newState);
}

I would also consider maybe changing up your design a little https://stackblitz.com/edit/react-6enuud
rather than handling each individual click out side, if it is just for display purposes, then it can be easier to encapsulate in a new component:
const FooBarDisplay = (entry) => {
const [active, setActive] = useState(entry.active);
const onClick = () => {
setActive(!active);
}
return (<p onClick={() => onClick()}>
{`Entry is: ${active ? 'active' : 'not active'}`}
</p>
)
}
Here you can make handling state easier, and avoid mutating arrays.
Simpler parent:
const FooBar = ({
fooList = [
{ active: false },
{ active: false }
]
}) => fooList.map((entry, i) => <FooBarDisplay key={i} entry={entry} />);
I've just moved default props to actual default argument values.

Related

React Jsx set checked state to false (reset button)

Here I'm trying to reset selected radio buttons on this list,
however it doesn't work because
I previously change input check from {checked} to {user.checked}. Refer from UserListElement.tsx below
Therefore, I tried the following two methods.
in useEffect(), set user.userId = false
useEffect(() => {
user.checked = false;
}, [isReset, user]);
→ no change.
setChecked to true when addedUserIds includes user.userId
if (addedUserIds.includes(`${user.userId}`)) {
setChecked(true);
}
→ Unhandled Runtime Error
Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.
Any suggestion on how to make this this work?
UserListElement.tsx
export const UserListElement = ({
user,
handleOnMemberClicked,
isReset,
}: {
user: UserEntity;
handleOnMemberClicked: (checked: boolean, userId: string | null) => void;
isReset: boolean;
}) => {
const [checked, setChecked] = useState(user.checked);
const addedUserIds = addedUserList.map((item) => item.userId) || [];
const handleOnChange = (e: ChangeEvent<HTMLInputElement>) => {
const checkedState = e.target.checked;
setChecked(checkedState); //not called
user.checked = checkedState;
handleOnMemberClicked(checkedState, user.userId);
};
useEffect(() => {
setChecked(false);
}, [isReset, user]);
if (addedUserIds.includes(`${user.userId}`)) {
user.checked = true;
// setChecked(true) cause runtime error (infinite loop)
}
return (
<li>
<label className={style.checkboxLabel}>
<input
type="checkbox"
className={style.checkboxCircle}
checked={user.checked}
// checked={checked}
onChange={(e) => handleOnChange(e)}
/>
<span>{user.name}</span>
</label>
</li>
);
};
UserList.tsx
export const UserList = (props: {
showsUserList: boolean;handleClose: () => void;corporationId: string;currentUserId: string;samePerson: boolean;twj: string;
}) => {
const [isReset, setReset] = useState(false);
.......
const resetAll = () => {
setReset(!isReset);
setCount((addedUserList.length = 0));
setAddedUserList([]);
setUserName('');
};
......
return ( <
> < div > xxxxx <
ul className = {
`option-module-list no-list option-module-list-member ${style.personListMember}`
} > {searchedUserList.map((user, i) => (
<UserListElement user = { user }
handleOnMemberClicked = { handleOnMemberClicked }
isReset = { isReset }
key = {i} />
)) }
</ul>
/div>
<a className="is-secondary reservation-popup-filter-reset" onClick={resetAll}>
.....
}
UseAddUserList.tsx
export class UserDetail {
constructor(public userId: string | null, public name: string | null) {}
}
export let addedUserList: UserDetail[] = [];
export let setAddedUserList: Dispatch<SetStateAction<UserDetail[]>>;
export const useAddUserList = (idList: UserDetail[]) => {
[addedUserList, setAddedUserList] = useState(idList);
};
Further Clarification:
Default view
Searched option (showed filtered list)
I use user.checked because when using only checked, the checked state does not carry on from filtered list view to the full view (ex. when I erase searched word or close the popup).
The real answer to this question is that the state should NOT be held within your component. The state of checkboxes should be held in UsersList and be passed in as a prop.
export const UserListElement = ({
user,
handleOnMemberClicked,
isChecked
}: {
user: UserEntity;
handleOnMemberClicked: (checked: boolean, userId: string | null) => void;
isChecked: boolean;
}) => {
// no complicated logic in here, just render the checkbox according to the `isChecked` prop, and call the handler when clicked
}
in users list
return searchedUserList.map(user => (
<UserListElement
user={user}
key={user.id}
isChecked={addedUserIds.includes(user.id)} <-- THIS LINE
handleOnMemberClicked={handleOnMemberClicked}
/>
)
You can see that you almost had this figured out because you were doing this in the child:
if (addedUserIds.includes(`${user.userId}`)) {
user.checked = true;
// setChecked(true) cause runtime error (infinite loop)
}
Which indicates to you that the checkdd value is entirely dependent on the state held in the parent, which means there is actually no state to be had in the child.
Also, in React, NEVER mutate things (props or state) like - user.checked = true - that's a surefire way to leave you with a bug that will cost you a lot of time.
Hopefully this sheds some light
In your UserListElement.tsx you are setting state in render, which triggers renders the component again, and again set the state which again triggers re-render and the loop continues. Try to put your condition in the useEffect call, also you mutate props, so don't set user.checked = true. Instead call setter from the parent component, where it is defined.
useEffect(() => {
setChecked(false);
if (addedUserIds.includes(user.userId)) {
setChecked(true);
}
}, [user]);

React - Update non stateful data inside reducer

I am implementing a context that manages all the messages of a conversation.
To reduce the complexity of my algorithm, I have decided to use a Map "sectionsRef" for accessing some stuff in O(1).
This map, needs to be updated inside my reducer's logic, where I update the stateful data, in order to synchronize both.
export function MessagesProvider({ children }) {
const [messages, dispatch] = useReducer(messagesReducer, initialState);
const sectionsRef = useMemo(() => new Map(), []);
const addMessages = (messages, unshift = false) => {
dispatch(actionCreators.addMessages(messages, unshift));
};
const addMessage = (message) => addMessages([message]);
const deleteMessage = (messageId) => {
dispatch(actionCreators.deleteMessage(messageId));
};
const value = useMemo(() => ({
messages,
addMessages,
deleteMessage,
// eslint-disable-next-line react-hooks/exhaustive-deps
}), [messages]);
return (
<MessagesContext.Provider value={value}>
{children}
</MessagesContext.Provider>
);
}
As you can see, I am using useMemo when initializing the Map in order to prevent re-initializations due to re-renders.
Is it correct to pass it as a payload to my reducer actions?
const addMessages = (messages, unshift = false) => {
dispatch(actionCreators.addMessages(messages, unshift, sectionsRef)); <---
};
To simplify my problem, imagine this is the real code:
//
// Reducer action
//
function reducerAction(state, messages, sectionsRef, title) {
state.push(...messages);
sectionsRef.set(title, state.length - 1);
}
//
// Context code
//
const state = [];
const firstMessagesSection = [{ id: 1 }];
const secondMessagesSection = [{ id: 1 }, { id: 2 }]
const sectionsRef = new Map();
reducerAction(state, firstMessagesSection, sectionsRef, "first section");
reducerAction(state, secondMessagesSection, sectionsRef, "second section");
console.log(state);
console.log(sectionsRef.get("second section"));
I am asking this because I have read that we shouldn't run side effects inside the reducers logic... so, if I need to synchronize that map with the state, what should I do instead?
Is it correct to pass it as a payload to my reducer actions?
No: reducers must be pure functions.
Redux describes reducers using a short list which I think is very useful:
Rules of Reducers​
We said earlier that reducers must always follow some special rules:
They should only calculate the new state value based on the state and action arguments
They are not allowed to modify the existing state. Instead, they must make immutable updates, by copying the existing state and making changes to the copied values.
They must not do any asynchronous logic or other "side effects"
The second and third items together describe pure functions, and the first one is just a Redux-specific convention.
In your example, you are violating two rules of pure functions:
mutating state with state.push(...messages) (rather than creating a new array and returning it), and
performing side-effects by modifying a variable in the outer scope: sectionsRef.set(title, state.length - 1)
Further, you seem to never use the Map (how is it accessed in your program?). It should be included in your context, and you can simply define it outside your component (its identity will never change so it won't cause a re-render).
Here's how you can refactor your code to achieve your goal:
Keep the reducer data pure:
// store.js
export function messagesReduer (messages, action) {
switch (action.type) {
case 'ADD': {
const {payload, unshift} = action;
return unshift ? [...payload, ...messages] : [...messages, ...payload];
}
case 'DELETE': {
const {payload} = action;
return messages.filter(m => m.id !== payload);
}
}
}
export const creators = {};
creators.add = (messages, unshift = false) => ({type: 'ADD', payload: messages, unshift});
creators.delete = (id) => ({type: 'DELETE', payload: id});
export const sections = new Map();
Update the Map at the same that you dispatch an action to the related state by combining those operations in a function:
// MessagesContext.jsx
import {
createContext,
useCallback,
useMemo,
useReducer,
} from 'react';
import {
creators,
messagesReduer,
sections,
} from './store';
export const MessagesContext = createContext();
export function MessagesProvider ({ children }) {
const [messages, dispatch] = useReducer(messagesReducer, []);
const addMessages = useCallback((title, messages, unshift = false) => {
dispatch(creators.add(messages, unshift));
sections.set(title, messages.length);
}, [creators.add, dispatch, messages]);
const addMessage = useCallback((title, message, unshift = false) => {
dispatch(creators.add([message], unshift));
sections.set(title, messages.length);
}, [creators.add, dispatch, messages]);
const deleteMessage = useCallback((id) => {
dispatch(creators.delete(id));
}, [creators.delete, dispatch]);
const value = useMemo(() => ({
addMessage,
addMessages,
deleteMessage,
messages,
sections,
}), [
addMessage,
addMessages,
deleteMessage,
messages,
sections,
]);
return (
<MessagesContext.Provider value={value}>
{children}
</MessagesContext.Provider>
);
}
Use the context:
// App.jsx
import {useContext} from 'react';
import {MessagesContext, MessagesProvider} from './MessagesContext';
function Messages () {
const {
// addMessage,
// addMessages,
// deleteMessage,
messages,
// sections,
} = useContext(MessagesContext);
return (
<ul>
{
messages.map(({id}, index) => (
<li key={id}>Message no. {index + 1}: ID {id}</li>
))
}
</ul>
);
}
export function App () {
return (
<MessagesProvider>
<Messages />
</MessagesProvider>
);
}
Additional notes:
Make sure your dependency lists (e.g. in useMemo, etc.) are exhaustive. Those lint warnings are there to help prevent you from making mistakes. In general, you should never need to suppress them.

Update State of Another Component Within A Generic Component Causes Warning

When designing a generic component, I adopted the principle of SOC where the generic component will not know the implementation details of the user and allow the user to listen for callbacks to handle their own state.
For example:
SortButton.tsx
interface Props {
initialValue?: boolean;
onToggle?: (value: boolean) => void;
}
const Toggle: React.FC<Props> = ({
initialValue = false,
onToggle,
}) => {
const [isActive, setIsActive] = React.useState(initialValue);
const handleToggle = React.useCallback(() => {
let updatedValue = isActive;
// do some computation if needed
if(onToggle) {
onSort(updatedValue);
}
setIsActive(updatedValue);
}, [isActive, onToggle]);
return (
<div onClick={handleToggle}>Sort</div>
);
}
Parent.tsx
const Parent: React.FC<Props> = ({
}) => {
const [parentObject, setParentObject] = React.useState({});
const handleToggle = React.useCallback((value: boolean) => {
let updatedValue = value;
// do some computation to updatedValue if needed
setParentObject((parent) => { ...parent, calculated: updatedValue });
}, []);
return (
<SortButton onToggle={handleToggle} />
);
}
The above implementation allows the generic component (ie. Toggle) to handle their own state and allows parents to implement their own implementation using callbacks. However, I have been getting the following error:
Warning: Cannot update a component (`Parent`) while rendering a different component (`Toggle`). To locate the bad setState() call inside `Toggle`
My understanding from the error is that, within the callback onClick, it triggers state mutation of the parent (in this case, another component) which should not be allowed. The solution I figured was to move the callback within an useEffect like this:
SortButton.tsx
interface Props {
initialValue?: boolean;
onToggle?: (value: boolean) => void;
}
const Toggle: React.FC<Props> = ({
initialValue = false,
onToggle,
}) => {
const [isActive, setIsActive] = React.useState(initialValue);
React.useEffect(() => {
let updatedValue = isActive;
// do some computation if needed
if(onToggle) {
onSort(updatedValue);
}
}, [isActive, onToggle]);
const handleToggle = React.useCallback(() => {
setIsActive((value) => !value);
}, []);
return (
<div onClick={handleToggle}>Sort</div>
);
}
The new implementation works but I have a few questions which I would hope to get some guidance on.
The example works and is easy to refactor because it is a simple state of isActive. What if the value we need is more complicated (for example, mouse position, etc) and does not have a state to store the value and is only available from onMouseMove? Do we create a state to store the `mouse position and follow the pattern?
Is the existing implementation an anti-pattern to any of the React concepts in the first place?
Is there any other possible implementation to solve the issue?
This is a somewhat biased opinion, but I'm a big proponent of Lifting State Up and "Dumb" Components/Controlled Components.
I would design it so that the SortButton does not have any internal state. It would get all of the information that it needs from props. The Parent would be responsible for passing down the correct value of isActive/value, which it will update when the child SortButton calls its onToggle prop.
We can include the event in the onToggle callback just in case the parent wants to use it.
SortButton.tsx
import * as React from "react";
interface Props {
isActive: boolean;
onToggle: (value: boolean, e: React.MouseEvent<HTMLDivElement>) => void;
}
const Toggle: React.FC<Props> = ({ isActive, onToggle }) => {
return (
<div
className={isActive ? "sort-active" : "sort-inactive"}
onClick={(e) => onToggle(!isActive, e)}
>
{isActive ? "Unsort" : "Sort"}
</div>
);
};
export default Toggle;
Parent.tsx
import * as React from "react";
import SortButton from "./SortButton";
interface Props {
list: number[];
}
const Parent: React.FC<Props> = ({ list }) => {
// parent stores the state of the sort
const [isSorted, setIsSorted] = React.useState(false);
// derived data is better as a memo than as state.
const sortedList = React.useMemo(
// either sort the list or don't.
() => (isSorted ? [...list].sort() : list),
// depends on the list prop and the isSorted state.
[list, isSorted]
);
return (
<div>
<SortButton
isActive={isSorted}
// you could use a more complicated callback, but it's not needed here.
onToggle={setIsSorted}
/>
<ul>
{sortedList.map((n) => (
<li>{n.toFixed(3)}</li>
))}
</ul>
</div>
);
};
export default Parent;
Code Sandbox Demo

Too many re-renders - while trying to put props(if exists) in state

I am transfer an props from father component to child component.
On the child component I want to check if the father component is deliver the props,
If he does, i"m putting it on the state, If not I ignore it.
if(Object.keys(instituteObject).length > 0)
{
setInnerInstitute(instituteObject)
}
For some reason the setInnerInstitute() take me to infinite loop.
I don't know why is that happening and how to fix it.
getInstitutesById() - Is the api call to fetch the objects.
Father component(EditInstitute):
const EditInstitute = props => {
const {id} = props.match.params;
const [institute, setInstitute] = useState({})
useEffect(() => { //act like componentDidMount
getInstitutesById({id}).then((response) => {
setInstitute(response)
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return (
<React.Fragment>
<InstituteForm instituteObject={institute.object}/>
</React.Fragment>
)
}
Child component(InstituteForm):
const InstituteForm = (props) => {
const {instituteObject = {}} = props // if not exist default value = {}
const [innerInstitute, setInnerInstitute] = useState({})
if (Object.keys(instituteObject).length > 0) // if exists update the state.
{
setInnerInstitute(instituteObject)
}
return (
<React.Fragment>
not yet.
</React.Fragment>
)
}
Thanks
I think the way you are changing your InstituteForm's state causing this error. You can try using the useEffect hook to change your innerInstitute based on instituteObject. That's why you need to also add instituteObject in the dependency array of that useEffect hook.
import { useEffect, useState } from "react"
const InstituteForm = (props) => {
const {instituteObject = {}} = props // if not exist default value = {}
const [innerInstitute, setInnerInstitute] = useState({})
useEffect(() => {
// this is be evoked only when instituteObject changes
if (Object.keys(instituteObject).length > 0){
setInnerInstitute(instituteObject)
}
}, [instituteObject])
return (
<React.Fragment>
not yet.
</React.Fragment>
)
}

creating a redux like stores using custom hooks

Here I implemented redux like store using custom hooks. everything goes well and code executed correctly but problem is that in reducer under switch statement "TOGGLE" there I return a updated state which is finally stored in globalstate but if I returned empty object {} instead of {products: updated} still globalstate updating correctly with a change that has been done in reducer...since i am not passing globalstate reference then how it is updated correctly
and what listeners exactly do in dispatch method in code
import MarkFavMulti from "./MarkFavMulti";
import classes from "./MarkFav.module.css";
import useStore from "../HookStore/Store";
import {reducer2} from "../SampleReducer";
const MarkFav = props => {
const [outfit, dispatch] = useStore(reducer2);
const onClicked = (id) => {
dispatch({type: "TOGGLE", id: id});
}
const element = outfit.products.map((item) => {
return <MarkFavMulti cloth={item.name}
favorite={item.favorite}
price={item.price}
key={item.id}
clicked={onClicked.bind(this, item.id)} />
});
return (
<main className={classes.Markfav}>
{element}
</main>
);
};
export default MarkFav;
import {useState, useEffect} from "react";
let globalState = {};
let listeners = [];
const useStore = (reducer) => {
const setState = useState(globalState)[1];
const dispatch = (action) => {
let curr = Object.assign({},globalState);
const newState = reducer({...curr}, action)
globalState = {...globalState,...newState};
for(let listener of listeners) {
listener(globalState);
}
};
useEffect(()=>{
listeners.push(setState);
return () => {
listeners.filter(item => item !==setState);
}
},[setState]);
return [globalState, dispatch];
};
export const initStore = (initialState) => {
if(initialState) {
globalState = {...globalState, ...initialState};
}
}
export default useStore;
let initialState = {
products: [
{ id: 1, name: "shirt", price: "$12", favorite: false },
{ id: 2, name: "jeans", price: "$42", favorite: false },
{ id: 3, name: "coat", price: "$55", favorite: false },
{ id: 4, name: "shoes", price: "$8", favorite: false },
]
}
const configureStore = () => {
initStore(initialState);
};
export default configureStore;
export const reducer2 = (state=initialState, action) => {
switch (action.type) {
case "TOGGLE":
let update = {...state};
let updated = [...update.products];
updated = updated.map(item => {
if(item.id === action.id) {
item.favorite = !item.favorite;
return item;
}
return item;
});
return {products: updated};
//if we return {} ...it will updated correctly in globalstate
default:
throw new Error("not reachable");
}
}
The behavior that you are describing is due to this object assignment right here:
item.favorite = !item.favorite;
Here you are directly mutating the properties of the item object. You probably thought that it would be fine since you are using a copy of the products array.
let update = {...state};
let updated = [...update.products];
What actually happens is that updated is a "shallow copy" of the original array. The array itself is a new array, but the items in that array are the same items as in the state. You can read more about that here.
You need to return a new item object instead of mutating it. Here's a concise way to write it using the ternary operator.
case "TOGGLE":
return {
...state, // not actually necessary since products is the only property
products: state.products.map((item) =>
item.id === action.id
? {
...item,
favorite: !item.favorite
}
: item
)
};

Categories

Resources