Related
I'm trying to build a 'to-do' list for a task. Initial state must have the structure shown on code. I'm new to coding and cannot figure out how to delete an object from an object array.
I have tried using the .pop() and .filter() methods but they are not accepted because the object array is an object of objects and not an actual array. I also tried to find the index and do delete state.data[index] but the console sends an error message "cannot update component while rendering other component". Rest of the code works fine when I don't include the handleDeleteClick() function and remove the deleteItem reducer. Here's the code:
//the following creates an item component for each item in the 'to do' list
import React, {useState} from 'react';
import { useDispatch } from 'react-redux';
import { editItem, deleteItem, completedItem } from '../store/todoSlice';
const TodoItem = ({ id, content, completed }) => {
const dispatch = useDispatch();
//initialising state for items that the user wants to edit
const [edit, setEdit] = useState(false);
const [newItem, setNewItem] = useState(content);
const [finishedItem, setFinishedItem]= useState(false);
//function to call deleteItem reducer
const handleDeleteClick = () => {
dispatch(deleteItem({id, content}))
}
//function to call editItem reducer
const onSubmit = (event) => {
event.preventDefault();
dispatch(
editItem({id, newItem, completed}));
//setting edit and finished state back to null
setNewItem("");
setEdit(false);
setFinishedItem(false);
};
//function to call completedItem reducer
const completedTask = (event) => {
event.preventDefault();
dispatch(
completedItem({id, content, completed})
);
setFinishedItem(true);
};
//if edit state is true, return <input> element to edit the item requested
if(edit){
return (
<form>
<input id="editInput" value={newItem} onChange= {(e) => setNewItem(e.target.value)} placeholder="Edit your item"/>
<button onClick = {onSubmit} type='submit' id="submitButton">ADD</button>
</form>
)
}
//if edit state is false and finishedItem is true, return same list and add an id to completed button
if(!edit && finishedItem) {
return(
<div id="itemSection">
<li id="item">{content}
<button onClick= {handleDeleteClick(content)} className="function">DELETE</button>
<button onClick={() => setEdit(true)} className="function"> EDIT</button>
<button onClick={completedTask} id="completed">COMPLETED</button>
</li>
</div>
)
}
//else, return <ul> element for each 'todo' item with 3 buttons to edit, delete or complete task
return (
<div id="itemSection">
<li id="item">{content}
<button onClick={handleDeleteClick()} className="function">DELETE</button>
<button onClick={() => setEdit(true)} className="function"> EDIT</button>
<button onClick={completedTask}>COMPLETED</button>
</li>
</div>
);
};
export default TodoItem;
//the following creates state slice for the todos object array
import { createSlice } from "#reduxjs/toolkit";
export const toDoSlice = createSlice({
name: "todos",
//set initial state of object array
initialState: {
nextId: 2,
data:
{
1: {
content: 'Content 1',
completed: false
}
}
},
reducers: {
//function to add item to object array
addItem: (state, action) => {
state.data =
{
...state.data,
[state.nextId]: {
content: action.payload.content,
completed: false
}
}
state.nextId += 1;
},
//function to delete item from object array
deleteItem: (state, action) => {
const index= action.payload.id;
delete state.data[index];
},
//function to edit item from object array
editItem: (state, action) => {
state.data =
{
...state.data,
[action.payload.id]: {
content: action.payload.newItem,
completed: false
}
}
},
//function to complete item from object array
completedItem: (state, action) => {
state.data =
{
...state.data,
[action.payload.id]: {
content: action.payload.content,
completed: true
}
}
}
}
});
export const {addItem, editItem, deleteItem, completedItem} =
toDoSlice.actions;
export default toDoSlice.reducer;
The problem with your example is that you're setting up data as an object. You should not do that, unless you have a good reason to, which doesn't seem to be the case.
Instead of:
createSlice({
//...
initialState: {
nextId: 2,
data: { // ๐ object
1: {
content: 'Content 1',
completed: false
}
}
}
// ...
})
you should use:
createSlice({
// ...
initialState: {
nextId: 2,
data: [{ // ๐ array
content: 'Content 1',
completed: false
}]
}
// ...
})
Now data has all the array methods available, including .filter(). 1
If, for whatever reason, you want to keep data as an object, you could use
delete data[key]
where key is the object property you want to delete. (e.g: if you want to delete 1, use delete state.data.1 or delete state.data['1']).
But my strong advice is to change data to an array.
Notes:
1 - Note you will need to modify all your reducers to deal with the array. For example:
{
reducers: {
addItem: (state, action) => {
state.data.push({
content: action.payload.content,
completed: false
})
}
}
}
Most likely, you won't need state.nextId anymore. That's the advantage of dealing with arrays, you don't need to know what key/index you're assigning to when you add an item.
You will likely need to add an unique identifier to each item (e.g: an id) so you can find it by that id when you want to delete or modify it.
I'm trying to delete item in redux toolkit, but don't know how, the remove function only work on screen, i have to press twice to delete the previous one,
Here is the reducer
const noteReducer = createSlice({
name: "note",
initialState: NoteList,
reducers: {
addNote: (state, action: PayloadAction<NoteI>) => {
const newNote: NoteI = {
id: new Date(),
header: action.payload.header,
note: action.payload.note,
date: new Date(),
selectStatus: false,
};
state.push(newNote);
},
removeNote: (state, action: PayloadAction<NoteI>) => { //
======> Problem here
return state.filter((item) => item.id !== action.payload.id);
},
toggleSelect: (state, action: PayloadAction<NoteI>) => {
return state.map((item) => {
if (item.id === action.payload.id) {
return { ...item, selectStatus: !item.selectStatus };
}
return item;
});
},
loadDefault: (state) => {
return state.map((item) => {
return { ...item, selectStatus: false };
});
},
resetNote: (state) => {
return (state = []);
},
editNote: (state, action: PayloadAction<NoteI>) => {
return state.map((item) => {
if (item.id === action.payload.id) {
return {
...item,
note: action.payload.note,
header: action.payload.header,
date: action.payload.date,
};
}
return item;
});
},
},
extraReducers: (builder) => {
builder.addCase(fetchNote.fulfilled, (state, action) => {
state = [];
return state.concat(action.payload);
});
},
});
Here is the function where i use it:
export default function NoteList(props: noteListI) {
const { title, note, id, date } = props;
const data = useSelector((state: RootState) => state.persistedReducer.note);
const removeSelectedNote = () => {
dispatch(removeNote({ id: id }));
console.log(data); ====> still log 4 if i have 4
};
return (
<View>
<TouchableOpacity
onLongPress={() => {
removeSelectedNote();
}}
// flex
style={CONTAINER}
onPress={() =>
!toggleSelectedButton ? onNavDetail() : setEnableToggle()
}
>
<Note
note={note}
header={title}
date={date}
id={id}
selectedStatus={selectedButtonStatus}
/>
</TouchableOpacity>
</View>
);
}
I have to press twice to make it work, for example, i have 4 item, when i press one, the item on screen disappears but the data log still have 4 item, when i click another, it show 3 on console.log but the screen display 2, i mean the function maybe work correctly but i want to update the state also, how can i do that?
Or how can i update the state if i remove item in redux-toolkit?
When i log the data on the redux, it return correct: 3
Here is a gif to show what going on
UPDATED
As #Janik suggest, i use console.log in function, so it log correct
But how can i get this change? I mean, it log correct, but i was fetch data from firebase so i need to log this data to make change to firebase, so how can i do that, i try to put it in a function:
const getNote = useCallback(() => {
setCurrentNote(data);
}, [data]);
But it show this error:
ExceptionsManager.js:184 Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
Where is your logged data coming from?
I suppose this is just a matter of order and timing, when your log happens within the React Lifecycle โUpdateโ.
If data references your state:
component is rendered initially, data is 4.
Note removed, still in the same rendering state, therefore data still is 4
React re-renders your component, data is 3.
To check on this, you can try changing the order by moving the console.log outside of the removeSelectedNote. This way, log will happen on step 1 and 3 instead of 2
I am learning React Reducer now. I want to build a toggle button that changes a boolean completed value to its opposite each time I click the button.
What I have is an array of states, each state is an object with an id and a completed value set to be true or false. Then I loop through states, setting each state as an Item component and display it on screen.
// App.js file
import React, { useReducer } from "react";
import { AppReducer } from "./AppReducer";
import Item from "./Item";
function App() {
const initialStates = [
{
id: 1,
completed: false,
},
{
id: 2,
completed: false,
},
];
const [states, dispatch] = useReducer(AppReducer, initialStates);
return (
<div>
{states.map((state) => (
<Item item={state} key={state.id} dispatch={dispatch} />
))}
</div>
);
}
export default App;
In the Item component, I display whether this item is completed or not (true or false). I set up a toggle function on the button to change the completed state of the Item.
// Item.js
import React from "react";
const Item = ({ item, dispatch }) => {
function setButtonText(isCompleted) {
return isCompleted ? "True" : "False";
}
let text = setButtonText(item.completed);
function toggle(id){
dispatch({
type: 'toggle',
payload: id
})
text = setButtonText(item.completed);
}
return (
<div>
<button type="button" onClick={() => toggle(item.id)}>Toggle</button>
<span>{text}</span>
</div>
);
};
export default Item;
Here is my reducer function. Basically what I am doing is just loop through the states array and locate the state by id, then set the completed value to its opposite one.
// AppReducer.js
export const AppReducer = (states, action) => {
switch (action.type) {
case "toggle": {
const newStates = states;
for (const state of newStates) {
if (state.id === action.payload) {
const next = !state.completed;
state.completed = next;
break;
}
}
return [...newStates];
}
default:
return states;
}
};
So my problem is that the toggle button only works once. I checked my AppReducer function, it did change completed to its opposite value, however, every time we return [...newStates], it turned back to its previous value. I am not sure why is that. I appreciate it if you can give it a look and help me.
The code is available here.
Here is the working version forked from your codesandbox
https://codesandbox.io/s/toggle-button-forked-jy6jd?file=/src/Item.js
The store value updated successfully. The problem is the way of listening the new item change.
dispatch is a async event, there is no guarantee the updated item will be available right after dispatch()
So the 1st thing to do is to monitor item.completed change:
useEffect(() => {
setText(setButtonText(item.completed));
}, [item.completed]);
The 2nd thing is text = setButtonText(item.completed);, it will not trigger re-render. Therefore, convert the text to state and set it when item.completed to allow latest value to be displayed on screen
const [text, setText] = useState(setButtonText(item.completed));
I have improved your code, just replace your AppReducer code with below.
export const AppReducer = (states, action) => {
switch (action.type) {
case "toggle": {
const updated = states.map((state) =>
action.payload === state.id ? {
...state,
completed: !state.completed
} : { ...state }
);
return [...updated];
}
default:
return states;
}
};
Live demo
i am trying to paginate the data from my rest server using CoreUI Table Component.
i have problem getting the updated data from redux store after dispatch request in useEffect, i am using redux thunk, i know that dispatch is async, but is there a way to wait for the dispatch to be completed? i tired making the dispatch a Promise but it did not work.
I successfully get the updated result from action and reducer but in ProductsTable its the previous one, i checked redux devtools extension and i can see the state being changed.
i never get the latest value from store.
Also the dispatch is being called so many times i can see in the console window, it nots an infinite loop, it stops after sometime.
const ProductsTable = (props) => {
const store = useSelector((state) => state.store);
const dispatch = useDispatch();
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
const [page, setPage] = useState(1);
const [pages, setPages] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(5);
const [fetchTrigger, setFetchTrigger] = useState(0);
useEffect(() => {
setLoading(true);
const payload = {
params: {
page,
},
};
if (page !== 0)
dispatch(getAllProducts(payload));
console.log("runs:" + page)
console.log(store.objects)
if(!(Object.keys(store.objects).length === 0)){
setItems(store.objects.results)
setPages(store.objects.total.totalPages)
setLoading(false)
} else{
console.log("error")
setFetchTrigger(fetchTrigger + 1);
}
}, [page, fetchTrigger]);
return (
<CCard className="p-5">
<CDataTable
items={items}
fields={["title", "slug", {
key: 'show_details',
label: '',
_style: { width: '1%' },
sorter: false,
filter: false
}]}
loading={loading}
hover
cleaner
sorter
itemsPerPage={itemsPerPage}
onPaginationChange={setItemsPerPage}
<CPagination
pages={pages}
activePage={page}
onActivePageChange={setPage}
className={pages < 2 ? "d-none" : ""}
/>
</CCard>
)
}
export default ProductsTable
The reason the ProductsTable always has the previous state data is because the effect you use to update the ProductsTable is missing the store as dependency or more specifically store.objects.results; when the page and the fetchTrigger change the effect becomes stale because it isn't aware that when those dependencies change the effect should change.
useEffect(() => {
// store.objects is a dependency that is not tracked
if (!(Object.keys(store.objects).length === 0)) {
// store.objects.results is a dependency that is not tracked
setItems(store.objects.results);
// store.objects.total.totalPages is a dependency that is not tracked
setPages(store.objects.total.totalPages);
setLoading(false);
}
// add these dependencies to the effect so that everything works as expected
// avoid stale closures
}, [page, fetchTrigger, store.objects, store.objects.results, store.objects.total.totalPages]);
The dispatch is being called many times because you have a recursive case where fetchTrigger is a dependency of the effect but you also update it from within the effect. By removing that dependency you'll see much less calls to this effect, namely only when the page changes. I don't know what you need that value for because I dont see it used in the code you've shared, but if you do need it I recommend using the callback version of setState so that you can reference the value of fetchTrigger that you need without needing to add it as a dependency.
useEffect(() => {
// code
if (!(Object.keys(store.objects).length === 0)) {
// code stuffs
} else {
// use the callback version of setState to get the previous/current value of fetchTrigger
// so you can remove the dependency on the fetchTrigger
setFetchTrigger(fetchTrigger => fetchTrigger + 1);
}
// remove fetchTrigger as a dependency
}, [page, store.objects, store.objects.results, store.objects.totalPages]);
With those issues explained, you'd be better off not adding new state for your items, pages, or loading and instead deriving that from your redux store, because it looks like thats all it is.
const items = useSelector((state) => state.store.objects?.results);
const pages = useSelector((state) => state.store.objects?.total?.totalPages);
const loading = useSelector((state) => !Object.keys(state.store.objects).length === 0);
and removing the effect entirely in favor of a function to add to the onActivePageChange event.
const onActivePageChange = page => {
setPage(page);
setFetchTrigger(fetchTrigger => fetchTrigger + 1);
dispatch(getAllProducts({
params: {
page,
},
}));
};
return (
<CPagination
// other fields
onActivePageChange={onActivePageChange}
/>
);
But for initial results you will still need some way to fetch, you can do this with an effect that only runs once when the component is mounted. This should do that because dispatch should not be changing.
// on mount lets get the initial results
useEffect(() => {
dispatch(
getAllProducts({
params: {
page: 1,
},
})
);
},[dispatch]);
Together that would look like this with the recommended changes:
const ProductsTable = props => {
const items = useSelector(state => state.store.objects?.results);
const pages = useSelector(state => state.store.objects?.total?.totalPages);
const loading = useSelector(state => !Object.keys(state.store.objects).length === 0);
const dispatch = useDispatch();
const [page, setPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(5);
const [fetchTrigger, setFetchTrigger] = useState(0);
// on mount lets get the initial results
useEffect(() => {
dispatch(
getAllProducts({
params: {
page: 1,
},
})
);
},[dispatch]);
const onActivePageChange = page => {
setPage(page);
setFetchTrigger(fetchTrigger => fetchTrigger + 1);
dispatch(
getAllProducts({
params: {
page,
},
})
);
};
return (
<CCard className="p-5">
<CDataTable
items={items}
fields={[
'title',
'slug',
{
key: 'show_details',
label: '',
_style: { width: '1%' },
sorter: false,
filter: false,
},
]}
loading={loading}
hover
cleaner
sorter
itemsPerPage={itemsPerPage}
onPaginationChange={setItemsPerPage}
/>
<CPagination
pages={pages}
activePage={page}
onActivePageChange={onActivePageChange}
className={pages < 2 ? 'd-none' : ''}
/>
</CCard>
);
};
export default ProductsTable;
first of all you don't need any synchronous to achieve the results you want, you have to switch up your code so it doesn't use the state of react since you are already using some kind of global store ( i assume redux ); what you need to do is grab all the items straight from the store don't do an extra logic on the component (read for the separation of concerns); Also I would suggest to do the pagination on the server side not just paginate data on the front end. (getAllProducts() method to switch on fetching just a page of results and not all the products); Your code have alot of dispatches because you are using page and fetchTrigger as dependencies of useEffect hook that means every time the page or fetchTrigger value changes the code inside useEffect will run again resulting in another dispatch;
Here is a slightly modified part of your code, you need to add some extra stuff on your action and a loading param in your global state
const ProductsTable = (props) => {
const dispatch = useDispatch();
// PUT THE LOADING IN THE GLOBAL STATE OR HANDLE IT VIA A CALLBACK OR ADD A GLOBAL MECHANISM TO HANLE LOADINGS INSIDE THE APP
const loading = useSelector(() => state.store.loading)
const items = useSelector((state) => state.store.objects?.results); // ADD DEFAULT EMPTY VALUES FOR objects smthg like : { objects: { results: [], total: { totalPages: 0 } }}
const pages = useSelector((state) => state.store.objects?.total?.totalPages);
const [page, setPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(5);
const [fetchTrigger, setFetchTrigger] = useState(0); // I DONT UNDERSTAND THIS ONE
const fetchData = () => dispatch(getAllProducts({ params: { page }}));
useEffect(() => {
fetchData();
}, []);
return (
<CCard className="p-5">
<CDataTable
items={items}
fields={["title", "slug", {
key: 'show_details',
label: '',
_style: { width: '1%' },
sorter: false,
filter: false
}]}
loading={loading}
hover
cleaner
sorter
itemsPerPage={itemsPerPage}
onPaginationChange={setItemsPerPage}
<CPagination
pages={pages}
activePage={page}
onActivePageChange={setPage}
className={pages < 2 ? "d-none" : ""}
/>
</CCard>
)
}
export default ProductsTable
I am migrating my component from a class component to a functional component using hooks. I need to access the states with useSelector by triggering an action when the state mounts. Below is what I have thus far. What am I doing wrong? Also when I log users to the console I get the whole initial state ie { isUpdated: false, users: {}}; instead of just users
reducers.js
const initialState = {
isUpdated: false,
users: {},
};
const generateUsersObject = array => array.reduce((obj, item) => {
const { id } = item;
obj[id] = item;
return obj;
}, {});
export default (state = { ...initialState }, action) => {
switch (action.type) {
case UPDATE_USERS_LIST: {
return {
...state,
users: generateUsersObject(dataSource),
};
}
//...
default:
return state;
}
};
action.js
export const updateUsersList = () => ({
type: UPDATE_USERS_LIST,
});
the component hooks I am using
const users = useSelector(state => state.users);
const isUpdated = useSelector(state => state.isUpdated);
const dispatch = useDispatch();
useEffect(() => {
const { updateUsersList } = actions;
dispatch(updateUsersList());
}, []);
first, it will be easier to help if the index/store etc will be copied as well. (did u used thunk?)
second, your action miss "dispatch" magic word -
export const updateUsersList = () =>
return (dispatch, getState) => dispatch({
type: UPDATE_USERS_LIST
});
it is highly suggested to wrap this code with { try } syntax and be able to catch an error if happened
third, and it might help with the console.log(users) error -
there is no need in { ... } at the reducer,
state = intialState
should be enough. this line it is just for the first run of the store.
and I don't understand where { dataSource } comes from.