state is not managed correctly in react functional component - reactjs

I'm trying to work on a react page that has 3 states: View: 0, Edit and Add
the page state keeps track of current record id and the state page is in. Here are the rules.
record id can change only if page is in view mode
if page enters Edit/Add mode then unless user is out of that mode, record id must not change and the page state stays as it is
here is my code to achieve the same
interface IPageManager{
id: number,
mode: Modes,
grid: {page: number, size: number},
setId(id: number): void,
enterAddMode(): void,
enterEditMode(teamId: number): void,
enterViewMode(id?: number): void
debug(msg: string):void
}
type PageStateType = {
grid: {page: number, size: number},
id: number,
mode: Modes
}
function usePageManager(): IPageManager {
const [state, setState] = useState({ grid: {page: 1, size: 10}, id: 0, mode: Modes.View } as PageStateType)
const isEditing = () => state.mode === Modes.Add || state.mode === Modes.Edit
const change = (teamId?: number, mode: Modes= Modes.View)=>{
if(!isEditing())
setState(current=>produce(current, x=>{
if(teamId)
x.id = teamId
x.mode = mode
}))
}
const manager: IPageManager = {
get id() { return state.id }
, get mode() { return state.mode }
, get grid() { return state.grid }
, setId(id: number) {
setState(previous => produce(previous, x => { x.id = id }))
}
, enterAddMode() { change(undefined, Modes.Add) }
, enterEditMode(teamId: number) { change(teamId, Modes.Edit) }
, enterViewMode(id?: number){
if(!isEditing()){
setState(previous=>produce(previous, x=>{
x.mode = Modes.View
if(id)
x.id = id as number
}))
}
}
, debug(msg: string){
const mode = state.mode===Modes.View? 'View': state.mode===Modes.Edit? 'Edit':'Add'
trace(`${msg} - {id:${state.id}, mode:${mode}}`)
}
}
return manager
}
export function TeamsPage() {
const header = useMemo(() => <PageHeader title="Teams Management" />, [])
const manager = usePageManager()
manager.debug('page') // reflects correct state
const { data }= useQuery('teams', async () => await getTeams(manager.grid.page, manager.grid.size))
const view =(id: number)=>{
manager.debug(`before`) // manager state always at initial state
if(manager.mode===Modes.View) //always true since default is View
{
manager.setId(id)
manager.debug(`after`) // manager state always at initial state
}
}
const listing = useMemo(() => {
const { rows, count } = data ?? { rows: [], count: 0 } as TeamSummary
const t0 = (
<Listing
data={rows}
page={manager.grid.page}
size={manager.grid.size}
count={count}
onSelect={(arg) => view(arg.id) }
onAdd={() => manager.enterAddMode()}
onEdit={(arg) => manager.enterEditMode(arg.id)}
/>)
return t0
}, [data])
useEffect(() => { // if the data is updated, set the recordId to the first record in grid
const { rows, count } = data ?? { rows: [], count: 0 } as TeamSummary
if (rows && rows.length>0) {
const first = _.first(rows)
if (first)
manager.setId(first.id)
}
}, [data])
// it renders either an editor or a viewer, does not mantain any state on its own. depends on props only
const detail =<RecordBar id={manager.id} mode={manager.mode} getRecord={getTeam} emptyRecord={() => emptyTeamDetailResponse} />
return (
<Layout
summary={listing}
detail={detail}
header={header}
footer={<></>}
/>
)
}
In my opinion the page should work correctly but this is what I see in output
As you can see, after entering the Edit mode, page does change the current id despite the fact that if condition checks if the page is in view state or not before making any changes. To that if condition, state is always initial state even though in page it is reflected correctly.
is it some sort of closure problem or react issue or my coding is incorrect?
is it the correct way to manage the view i.e. by creating a custom hook that holds the state and returns the functions to operate on state?

Related

React component uses old data from previous API call

I am using React Query to fetch data from an API I have built. The component is rendering the old data from the previous api call and not updating with new the data from the new api call.
The new data is only rendering when I refresh the page.
Component:
export const ProfilePageStats = (props: {
user: User;
id: number;
}) => {
const { chatId } = useParams();
const { status: subscribeStatus, data: subscribeData } =
useSubscriptionsWithType(
chatId ? chatId : "",
props.id,
props.user.id,
"SUBSCRIBE"
);
const { status: unsubscribeStatus, data: unsubscribeData } =
useSubscriptionsWithType(
chatId ? chatId : "",
props.id,
props.user.id,
"UNSUBSCRIBE"
);
if (unsubscribeStatus == "success" && subscribeStatus == "success") {
console.log("Working", unsubscribeData);
return (
<ProfilePageStatsWithData
user={props.user}
subscribed={Object.keys(subscribeData).length}
unsubscribed={Object.keys(unsubscribeData).length}
/>
);
}
if (unsubscribeStatus == "error" && subscribeStatus == "error") {
console.log("error");
return <ProfilePageStatsLoading />;
}
if (unsubscribeStatus == "loading" && subscribeStatus == "loading") {
console.log("loading");
return <ProfilePageStatsLoading />;
}
return <ProfilePageStatsLoading />;
};
export const useSubscriptionsWithType = (
chatId: string,
id: number,
userId: number,
type: string
) => {
return useQuery(
["subscriptionsWithType"],
async () => {
const { data } = await api.get(
`${chatId}/subscriptions/${id}/${userId}?type=${type}`
);
return data;
},
{
enabled: chatId > 0 && userId > 0,
refetchOnWindowFocus: false,
}
);
};
The component should update to show the new user values but shows the previous user values. If I click out and select a different user entirely it then shows the values for the previously clicked user.
I can see that React Query is fetching with the correct values for the query but the component still renders the old user data?
It turns out that the fetchStatus value is changing to "fetching" but it not actually calling the api. Hence, why its only using the old values?
Your key part of the useQuery is what tells the hook when to update.
You only use ["subscriptionsWithType"] as key, so it will never know that you need to refetch something.
If you add userId there, it will update when that changes.
So, using
return useQuery(
["subscriptionsWithType", userId],
async () => {
...
will work.
It is likely, that you want all the params, that you use in the url, to be added there.
I solved it by adding a useEffect and refetching based on the changing user id.
useEffect(() => {
refetch();
}, [props.user.id]);

Too many re-renders with useSelector hook closure

Considering this state, I need to select some data from it:
const initialState: PlacesStateT = {
activeTicket: null,
routes: {
departure: {
carriageType: 'idle',
extras: {
wifi_price: 0,
linens_price: 0,
},
},
arrival: {
carriageType: 'idle',
extras: {
wifi_price: 0,
linens_price: 0,
},
},
},
};
so, I came up with two approaches:
first:
const useCoaches = (dir: string) => {
const name = mapDirToRoot(dir);
const carType = useAppSelector((state) => state.places.routes[name].carriageType);
const infoT = useAppSelector((state) => {
return state.places.activeTicket.trainsInfo.find((info) => {
return info.routeName === name;
});
});
const coaches = infoT.trainInfo.seatsTrainInfo.filter((coach) => {
return coach.coach.class_type === carType;
});
return coaches;
};
and second:
const handlerActiveCoaches = (name: string) => (state: RootState) => {
const { carriageType } = state.places.routes[name];
const { activeTicket } = state.places;
const trainInfo = activeTicket.trainsInfo.find((info) => {
return info.routeName === name;
});
return trainInfo.trainInfo.seatsTrainInfo.filter((coach) => {
return coach.coach.class_type === carriageType;
});
};
const useActiveInfo = (dir: string) => {
const routeName = mapDirToRoot(dir);
const selectActiveCoaches = handlerActiveCoaches(routeName);
const coaches = useAppSelector(selectActiveCoaches);
return coaches;
};
Eventually, if the first one works ok then the second one gives a lot of useless re-renders in component. I suspect that there are problems with selectActiveCoaches closure, maybe react considers that this selector is different on every re-render but I am wrong maybe. Could you explain how does it work?
selectActiveCoaches finishes with return seatsTrainInfo.filter(). This always returns a new array reference, and useSelector will force your component to re-render whenever your selector returns a different reference than last time. So, you are forcing your component to re-render after every dispatched action:
https://react-redux.js.org/api/hooks#equality-comparisons-and-updates
One option here would be to rewrite this as a memoized selector with Reselect:
https://redux.js.org/usage/deriving-data-selectors

MutationObserver is reading old React State

I'm attempting to use a MutationObserver with the Zoom Web SDK to watch for changes in who the active speaker is. I declare a state variable using useState called participants which is meant to hold the information about each participant in the Zoom call.
My MutationObserver only seems to be reading the initial value of participants, leading me to believe the variable is bound/evaluated rather than read dynamically. Is there a way to use MutationObserver with React useState such that the MutationCallback reads state that is dynamically updating?
const [participants, setParticipants] = useState({});
...
const onSpeechMutation = (mutations) => {
mutations.forEach((mutation) => {
// identify name of speaker
if(name in participants) {
// do something
} else {
setParticipants({
...participants,
[name] : initializeParticipant(name)
})
}
})
}
...
useEffect(() => {
if(!speechObserverOn) {
setSpeechObserverOn(true);
const speechObserver = new MutationObserver(onSpeechMutation);
const speechConfig = {
attributes: true,
attributeOldValue: true,
attributeFilter: ['class'],
subtree: true,
}
const participantsList = document.querySelector('.participants-selector');
if(participantsList) {
speechObserver.observe(participantsList, speechConfig);
}
}
}, [speechObserverOn])
If you are dealing with stale state enclosures in callbacks then generally the solution is to use functional state updates so you are updating from the previous state and not what is closed over in any callback scope.
const onSpeechMutation = (mutations) => {
mutations.forEach((mutation) => {
// identify name of speaker
if (name in participants) {
// do something
} else {
setParticipants(participants => ({
...participants, // <-- copy previous state
[name]: initializeParticipant(name)
}));
}
})
};
Also, ensure to include a dependency array for the useEffect hook unless you really want the effect to trigger upon each and every render cycle. I am guessing you don't want more than one MutationObserver at-a-time.
useEffect(() => {
if(!speechObserverOn) {
setSpeechObserverOn(true);
const speechObserver = new MutationObserver(onSpeechMutation);
const speechConfig = {
attributes: true,
attributeOldValue: true,
attributeFilter: ['class'],
subtree: true,
}
const participantsList = document.querySelector('.participants-selector');
if(participantsList) {
speechObserver.observe(participantsList, speechConfig);
}
}
}, []); // <-- empty dependency array to run once on component mount
Update
The issue is that if (name in participants) always returns false
because participants is stale
For this a good trick is to use a React ref to cache a copy of the current state value so any callbacks can access the state value via the ref.
Example:
const [participants, setParticipants] = useState([.....]);
const participantsRef = useRef(participants);
useEffect(() => {
participantsRef.current = participants;
}, [participants]);
...
const onSpeechMutation = (mutations) => {
mutations.forEach((mutation) => {
// identify name of speaker
if (name in participantsRef.current) {
// do something
} else {
setParticipants(participants => ({
...participants,
[name]: initializeParticipant(name)
}));
}
})
};

How to create multiple object in Reactjs?

I am having a onChange function i was trying to update the array optionUpdates which is inside of sentdata by index wise as i had passed the index to the onChange function.
Suppose i update any two values of the input field from option which is inside of postdata therefore the input name i.e. orderStatus with changed value and with order should be saved inside of optionUpdates
For example: Suppose i update the option 1 and option 3 of my postdata further inside of options of orderStatus values so my optionUpdates which is inside of sentdata should look like this
optionUpdates: [
{
Order: 1,
orderStatus: "NEW1"
},
{
Order: 3,
orderStatus: "New2"
}
]
here is what i tried
setSentData(oldValue => {
const curoptions = oldValue.sentdata.optionUpdates[idx];
console.log(curoptions);
curoptions.event.target.name = event.target.value;
return {
...oldValue,
sentdata: {
...oldValue.sentdata.optionUpdates,
curoptions
}
};
});
};
Demo
complete code:
import React from "react";
import "./styles.css";
export default function App() {
const x = {
LEVEL: {
Type: "LINEN",
options: [
{
Order: 1,
orderStatus: "INFO",
orderValue: "5"
},
{
Order: 2,
orderStatus: "INPROGRESS",
orderValue: "5"
},
{
Order: 3,
orderStatus: "ACTIVE",
orderValue: "9"
}
],
details: "2020 N/w UA",
OrderType: "Axes"
},
State: "Inprogress"
};
const [postdata, setPostData] = React.useState(x);
const posting = {
optionUpdates: []
};
const [sentdata, setSentData] = React.useState(posting);
const handleOptionInputChange = (event, idx) => {
const target = event.target;
setPostData(prev => ({
...prev,
LEVEL: {
...prev.LEVEL,
options: prev.LEVEL.options.map((item, id) => {
if (id === idx) {
return { ...item, [target.name]: target.value };
}
return item;
})
}
}));
setSentData(oldValue => {
const curoptions = oldValue.sentdata.optionUpdates[idx];
console.log(curoptions);
curoptions.event.target.name = event.target.value;
return {
...oldValue,
sentdata: {
...oldValue.sentdata.optionUpdates,
curoptions
}
};
});
};
return (
<div className="App">
{postdata.LEVEL.options.map((item, idx) => {
return (
<input
key={idx}
type="text"
name="orderStatus"
value={postdata.LEVEL.options[idx].orderStatus}
onChange={e => handleOptionInputChange(e, idx)}
/>
);
})}
</div>
);
}
If I've understood correctly then what you're looking to do is save a copy of the relevant options object in sentdata every time one changes. I think the best way to approach this is by doing all your state modification outside of setPostData, which then makes the results immediately available to both setPostData and setSentData. It will also make the setters easier to read, which is good because you have some quite deeply nested and complicated state here.
A few other things worth noting first:
Trying to use synchronous event results directly inside the asynchronous setter functions will throw warnings. If you do need to use them inside setters, then it is best to destructure them from the event object first. This implementation uses destructuring although it didn't end up being necessary in the end.
You seem to have got a bit muddled up with setSentData. The oldValue parameter returns the whole state, as prev in setPostData does. For oldValue.sentdata you just wanted oldValue. You also wanted curoptions[event.target.name], not curoptions.event.target.name.
So, on to your code. I would suggest that you change the way that your input is rendered so that you are using a stable value rather than just the index. This makes it possible to reference the object no matter which array it is in. I have rewritten it using the Order property - if this value is not stable then you should assign it one. Ideally you would use a long uuid.
{postdata.LEVEL.options.map(item => {
return (
<input
key={item.Order}
type="text"
name="orderStatus"
value={item.orderStatus}
onChange={e => handleOptionInputChange(e, item.Order)}
/>
);
})}
The handleOptionInputChange function will now use this Order property to find the correct objects in both postdata and sentdata and update them, or if it does not exist in sentdata then push it there. You would do this by cloning, modifying, and returning the relevant array each time, as I explained before. Here is the function again with comments:
const handleOptionInputChange = (event, orderNum) => {
const { name, value } = event.target;
/* Clone the options array and all objects
inside so we can mutate them without
modifying the state object */
const optionsClone = postdata.LEVEL.options
.slice()
.map(obj => Object.assign({}, obj));
/* Find index of the changed object */
const optionIdx = optionsClone.findIndex(obj => obj.Order === orderNum);
/* If the orderNum points to an existing object...*/
if (optionIdx >= 0) {
/* Change the value of object in clone */
optionsClone[optionIdx][name] = value;
/* Set postdata with the modified optionsClone */
setPostData(prev => ({
...prev,
LEVEL: {
...prev.LEVEL,
options: optionsClone
}
}));
/* Clone the optionUpates array and all
contained objects from sentdata */
const updatesClone = sentdata.optionUpdates
.slice()
.map(obj => Object.assign({}, obj));
/* Find the index of the changed object */
const updateIdx = updatesClone.findIndex(obj => obj.Order === orderNum);
/* If the changed object has already been
changed before, alter it again, otherwise push
a new object onto the stack*/
if (updateIdx >= 0) {
updatesClone[updateIdx][name] = value;
} else {
updatesClone.push({ Order: orderNum, [name]: value });
}
/* Set sentdata with modified updatesClone */
setSentData(prev => ({
...prev,
optionUpdates: updatesClone
}));
}
};

Duplication problem in TodoList application

I am creating a todo-list, the following function handleChange gets the id of the a todo component and changes its attribute of completed from true/false. This is then saved in state of allTodos
function handleChange(id) {
const updatedTodos = allTodos.map(todo => {
if (todo.id === id) {
todo.completed = !todo.completed
}
return todo
})
setTodos(updatedTodos)
}
const todoComponents = allTodos.map(item => <Todos key={item.id} item={item} handleChange={handleChange}/>)
the function updateDB takes that value from state and using it to update the database.
function updateDB(event) {
event.preventDefault()
const value = {
completed: false,
text: newTodo,
id: allTodos.length,
}
}
Here's where the problem arises: id: allTodos.length. If one of these are deleted, it will create a todo with a duplicate ID, crashing the whole thing. I don't know how to avoid this problem.
In updateDB, you are setting id to allTodos.length aka 1.

Resources