React Drag-and-Drop with useState is overwriting previous state - reactjs

I'm working on a page builder of sorts for my product, and I'm using react-dnd to accomplish what I need. So far I've gotten the basic drag-and-drop functionality taken care of, and I'm able to add a single item to my page, but I'm hitting a snag.
When I drag a new item to my drop zone, the expected behavior is that it will add a new item to the existing array that I've got stored in a nested object in state but, instead when I drag-and-drop another item to it it is overwriting the previous item, only allowing my to have a single item in my page. I have been beating my head against the wall all day. Below is my code; please comment if you need more and I will do my best to clarify.
So, here is my useState. I have a couple other items in here, but they're unimportant. All that matters is the content array:
const [lessonContent, setLessonContent] = useState({
title: '',
courseType: '',
content: [],
});
My function to add an element to lessonContent.content which is passed to the useDrop hook:
const handleAddLessonElement = (element: Record<string, unknown>) => {
setLessonContent({
...lessonContent,
content: [...lessonContent.content, element],
});
};
const [{ isOver }, drop] = useDrop(() => ({
accept: 'card',
drop: (item) => handleAddLessonElement(item),
collect: (monitor) => ({
isOver: !!monitor.isOver(),
}),
}));
The useDrag hook:
const [{ isDragging }, drag] = useDrag(() => ({
type: 'card',
item: {
type: element.value,
value: '',
config: { label: element.label },
},
collect: (monitor) => ({
isDragging: !!monitor.isDragging(),
}),
}));
Also, I'm using the <DNDProvider> with HTML5Backend, and I've got it wrapping all components that are utilizing tools from react-dnd. As I said the basics are working, but I seem to be messing something up in adding to state. I just cannot, for the life of me, find what it is.
Thanks in advance!

Ended up figuring out, turns out it was a solution I had tried but hadn't implemented correctly.
My problem is that I was not tracking the array as it updated, so I was constantly overwriting what I had just done. So, all it took was updating my handleAddLessonElement function to track the updated state before adding an element to the array.
const handleAddLessonElement = (element: Record<string, unknown>) => {
setLessonContent((previousState: Record<string, any>) => ({
...previousState,
content: [...previousState.content, element],
}));
};
As you can see, I added the previousState which will track the newly update state as I add elements, then use the spread operator to add these new elements to the copied array.

Related

How to save multi-selected options into an array within React state

I'm trying to save both team members in select into an array within a state object. My expectation would be to be able to ctrl-click the options and this would update state (using the on change handler). For right now I have this, which only ends up saving the first option in state:
setProjectDetails((prevState) => ({
...prevState,
[e.target.name]: e.target.value,
}));
I'm working with a mongoose/mongodb backend, so the id's are associated with the team members. I'm looking for the state to end up like this:
const initialProjectDetails = {
title: "test",
description: "test",
teamMembers: ["628c0133e5edf7b21cd8f31b", "628c0133e5edf7b21cd8f31b"], };
Form:
I think you forgot to update the desired key in the object (teamMembers):
setProjectDetails((prevState) => ({ ...prevState, teamMembers: e.target.value, }));

React setter function not updating state as expected

I am somewhat new to React and I am running into an issue and I was hoping someone will be willing to help me understand why my method is not working.
I have this state:
const [beers, setBeers] = useState([
{
id: 8759,
uid: "8c5f86a9-87bf-41fa-bc7f-044a9faf10be",
brand: "Budweiser",
name: "Westmalle Trappist Tripel",
style: "Fruit Beer",
hop: "Liberty",
yeast: "1056 - American Ale",
malts: "Special roast",
ibu: "22 IBU",
alcohol: "7.5%",
blg: "7.7°Blg",
bought: false
},
{
id: 3459,
uid: "7fa04e27-0b6b-4053-a26b-c0b1782d31c3",
brand: "Kirin",
name: "Hercules Double IPA",
style: "Amber Hybrid Beer",
hop: "Nugget",
yeast: "2000 - Budvar Lager",
malts: "Vienna",
ibu: "18 IBU",
alcohol: "9.4%",
blg: "7.5°Blg",
bought: true
}]
I am rendering the beers with a map function and I have some jsx that calls a handleClick function
<button onClick={() => handleClick(beer.id)}>
{beer.bought ? "restock" : "buy"}
</button>
this is the function being called:
const handleClick = (id) => {
setBeers((currentBeers) =>
currentBeers.map((beer) => {
if (beer.id === id) {
beer.bought = !beer.bought;
console.log(beer);
}
return beer;
})
);
};
I wanted to use an updater function to update the state, I am directly mapping inside the setter function and since map returns a new array, I thought everything would work correctly but in fact, it doesn't. It works only on the first button click and after that it stops updating the value.
I noticed that if I use this method:
const handleClick = (id) => {
const newbeers = beers.map((beer) => {
if (beer.id === id) {
beer.bought = !beer.bought;
}
return beer;
});
setBeers(newbeers);
};
Then everything works as expected.
Can someone help me understand why my first method isn't working?
OK, I think I have figured it out. The difference between my sandbox and your sandbox is the inclusion of <StrictMode> in the Index file. Removing this fixes the issue, but is not the correct solution. So I dug a little deeper.
What we all missed was that in your code you were modifying the previous state object that is passed in. You should instead be creating a new beer object and then modifying that. So this code works (I hope):
setBeers((currentBeers) =>
currentBeers.map((currentBeer) => { // changed beer to currentBeer
const beer = {...currentBeer};
if (beer.id === id) {
beer.bought = !beer.bought;
}
return beer;
)
});
I hope that this helps.
react does not deeply compares the object in the state. Since you map over beers and just change a property, they are the same for react and no rerender will happen.
You need to set the state with a cloned object.
e.g.:
import {cloneDeep} from 'lodash';
...
setBeers(
cloneDeep(currentBeers.map((beer) => {
if (beer.id === id) {
beer.bought = !beer.bought;
console.log(beer);
}
return beer;
})
)
);

React Hooks - keep arguments reference in state

I created a hook to use a confirm dialog, this hook provides the properties to the component to use them like this:
const { setIsDialogOpen, dialogProps } = useConfirmDialog({
title: "Are you sure you want to delete this group?",
text: "This process is not reversible.",
buttons: {
confirm: {
onPress: onDeleteGroup,
},
},
width: "360px",
});
<ConfirmDialog {...dialogProps} />
This works fine, but also I want to give the option to change these properties whenever is needed without declaring extra states in the component where is used and in order to achieve this what I did was to save these properties in a state inside the hook and this way provide another function to change them if needed before showing the dialog:
interface IState {
isDialogOpen: boolean;
dialogProps: TDialogProps;
}
export const useConfirmDialog = (props?: TDialogProps) => {
const [state, setState] = useState<IState>({
isDialogOpen: false,
dialogProps: {
...props,
},
});
const setIsDialogOpen = (isOpen = true) => {
setState((prevState) => ({
...prevState,
isDialogOpen: isOpen,
}));
};
// Change dialog props optionally before showing it
const showConfirmDialog = (dialogProps?: TDialogProps) => {
if (dialogProps) {
const updatedProps = { ...state.dialogProps, ...dialogProps };
setState((prevState) => ({
...prevState,
dialogProps: updatedProps,
}));
}
setIsDialogOpen(true);
};
return {
setIsDialogOpen,
showConfirmDialog,
dialogProps: {
isOpen: state.isDialogOpen,
onClose: () => setIsDialogOpen(false),
...state.dialogProps,
},
};
};
But the problem here is the following:
Arguments are passed by reference so if I pass a function to the button (i.e onDeleteGroup) i will keep the function updated to its latest state to perform the correct deletion if a group id changes inside of it.
But as I'm saving the properties inside a state the reference is lost and now I only have the function with the state which it was declared at the beginning.
I tried to add an useEffect to update the hook state when arguments change but this is causing an infinite re render:
useEffect(() => {
setState((prevState) => ({
...prevState,
dialogProps: props || {},
}));
}, [props]);
I know I can call showConfirmDialog and pass the function to update the state with the latest function state but I'm looking for a way to just call the hook, declare the props and not touch the dialog props if isn't needed.
Any answer is welcome, thank you for reading.
You should really consider not doing this, this is not a good coding pattern, this unnecessarily complicates your hook and can cause hard to debug problems. Also this goes against the "single source of truth" principle. I mean a situation like the following
const Component = ({title}: {title?: string}) => {
const {showConfirmDialog} = useConfirmDialog({
title,
// ...
})
useEffect(() => {
// Here you expect the title to be "title"
if(something) showConfirmDialog()
}, [])
useEffect(() => {
// Here you expect the title to be "Foo bar?"
if(somethingElse) showConfirmDialog({title: 'Foo bar?'})
}, [])
// But if the second dialog is opened, then the first, the title will be
// "Foo bar?" in both cases
}
So please think twice before implementing this, sometimes it's better to write a little more code but it will save you a lot debugging.
As for the answer, I would store the props in a ref and update them on every render somehow like this
/** Assign properties from obj2 to obj1 that are not already equal */
const assignChanged = <T extends Record<string, unknown>>(obj1: T, obj2: Partial<T>, deleteExcess = true): T => {
if(obj1 === obj2) return obj1
const result = {...obj1}
Object.keys(obj2).forEach(key => {
if(obj1[key] !== obj2[key]) {
result[key] = obj2[key]
}
})
if(deleteExcess) {
// Remove properties that are not present on obj2 but present on obj1
Object.keys(obj1).forEach(key => {
if(!obj2.hasOwnProperty(key)) delete result[key]
})
}
return result
}
const useConfirmDialog = (props) => {
const localProps = useRef(props)
localProps.current = assignChanged(localProps.current, props)
const showConfirmDialog = (changedProps?: Partial<TDialogProps>) => {
localProps.current = assignChanged(localProps.current, changedProps, false)
// ...
}
// ...
}
This is in case you have some optional properties in TDialogProps and you want to accept Partial properties in showConfirmDialog. If this is not the case, you could simplify the logic a little by removing this deleteExcess part.
You see that it greatly complicates your code, and adds a performance overhead (although it's insignificant, considering you only have 4-5 fields in your dialog props), so I really recommend against doing this and just letting the caller of useConfirmDialog have its own state that it can change. Or maybe you could remove props from useConfirmDialog in the first place and force the user to always pass them to showConfirmDialog, although in this case this hook becomes kinda useless. Maybe you don't need this hook at all, if it only contains the logic that you have actually shown in the answer? It seems like pretty much the only thing it does is setting isDialogOpen to true/false. Whatever, it's your choice, but I think it's not the best idea

How to migrate app state from React Context to Redux Tool Kit?

What I'm trying to achieve:
I have a NextJS + Shopify storefront API application. Initially I set up a Context api for the state management but it's not that efficient because it re-renders everything what's wrapped in it. Thus, I'm moving all state to the Redux Toolkit.
Redux logic is pretty complex and I don't know all the pitfalls yet. But so far I encounter couple problems. For example in my old Context API structure I have couple functions that take a couple arguments:
const removeFromCheckout = async (checkoutId, lineItemIdsToRemove) => {
client.checkout.removeLineItems(checkoutId, lineItemIdsToRemove).then((checkout) => {
setCheckout(checkout);
localStorage.setItem('checkout', checkoutId);
});
}
const updateLineItem = async (item, quantity) => {
const checkoutId = checkout.id;
const lineItemsToUpdate = [
{id: item.id, quantity: parseInt(quantity, 10)}
];
client.checkout.updateLineItems(checkoutId, lineItemsToUpdate).then((checkout) => {
setCheckout(checkout);
});
}
One argument (checkoutId) from the state and another one (lineItemIdsToRemove) extracted through the map() method.
Inside actual component in JSX it looks and evokes like this:
<motion.button
className="underline cursor-pointer font-extralight"
onClick={() => {removeFromCheckout(checkout.id, item.id)}}
>
How can I declare this type of functions inside createSlice({ }) ?
Because the only type of arguments reducers inside createSlice can take are (state, action).
And also is it possible to have several useSelector() calls inside one file?
I have two different 'Slice' files imported to the component:
const {toggle} = useSelector((state) => state.toggle);
const {checkout} = useSelector((state) => state.checkout);
and only the {checkout} gives me this error:
TypeError: Cannot destructure property 'checkout' of 'Object(...)(...)' as it is undefined.
Thank you for you're attention, hope someone can shad the light on this one.
You can use the prepare notation for that:
const todosSlice = createSlice({
name: 'todos',
initialState: [] as Item[],
reducers: {
addTodo: {
reducer: (state, action: PayloadAction<Item>) => {
state.push(action.payload)
},
prepare: (id: number, text: string) => {
return { payload: { id, text } }
},
},
},
})
dispatch(todosSlice.actions.addTodo(5, "test"))
But 99% of the cases you would probably stay with the one-parameter notation and just pass an object as payload, like
dispatch(todosSlice.actions.addTodo({ id: 5, text: "test"}))
as that just works out of the box without the prepare notation and makes your code more readable anyways.

How to use useState with {}?

given this..
const [question, setQuestion] = useState({})
and question can contain a title and a description.
I.E.
{
title: 'How to use useState with {}?',
decription: 'given this..`const [question, setQuestion] = useState({})`and `question` can contain a title and a description. I.E. { title: 'How to use useState with {}?', decription: '' }How can the title and description be set independently with `setQuestion`?'
}
How can the title and description be set independently with setQuestion?
The setter function you get from useState() expects to be passed argument which will entirely replace the old state. So you can't use it to update just the title, without also passing in all other properties.
But you can derive a new state object from the existing one and then pass that whole object to setQuestion()
setQuestion({
...question,
title: "New title",
})
One thing that I like to do in this situation is to use the React.useReducer hook instead:
function App() {
const initialState = {
title: 'My title',
description: 'My description'
}
const [state, setState] = React.useReducer(
(p, n) => ({ ...p, ...n }),
initialState
)
return (
<div>
<p>{state.title}</p>
<p>{state.description}</p>
<button onClick={() => setState({ description: 'New description' })}>
Set new description
</button>
</div>
)
}
This way, you're only changing the properties that you want and don't have to deal with copying the old state object and creating a new one based on old and new values.
Also, it will probably look more familiar to you if you're just starting with hooks because of the similarity to the this.setState() calls inside class components.
Here's a little example that shows this approach in action:
example
If this is a common pattern you use then I'd suggest writing your own hook to implement the functionality you want.
If it's a one-off thing then you can use the object spread operator to do it fairly cleanly:
setQuestion({
...question,
title: 'updated title',
});
Here is what it would look like pulled out into a separate hook:
const useMergeState = (initial) => {
const [state, setState] = React.useState(initial);
const mergeState = React.useCallback((update) => setState({...state, ...update}));
return [state, mergeState];
};
The official Docs says:
Both putting all state in a single useState call, and having a
useState call per each field can work. However, components tend to be most
readable when you find a balance between these two extremes, and group
related state into a few independent state variables.
Eg:
const [position, setPosition] = useState({ left: 0, top: 0 });
const [size, setSize] = useState({ width: 100, height: 100 });
In case the state logic becomes complex, we recommend managing it with a reducer or a custom Hook.
Also, remember in case you are using any state object in the useState({}) hook, that unlike the classic this.setState, updating a state variable always replaces it instead of merging it, so you need to spread the previous state in order not to lose some of his properties eg: setResource(…resource, name: ‘newName’);
setQuestion({
title: 'YOUR_TITLE',
description: 'YOUR_DESCRIPTION',
id: '13516',
})
State cannot be edited. You need to set new value always. Thus, it can be replaced with spread operator as below.
setQuestion({
...question,
title: 'new title'
})
or
const newQuestion = { ...question, title: 'new title }
setQuestion(newQuestion)

Resources