How to disable an active item after dispatch? - javascript

I would like to create notifications that expire after a set amount of seconds.
I have created a property which is 'active' and when toggled to false it will hide.
Ideally, it would be nice to have the expiry automatically set in the slice, i.e. run the disable reducer within the runtime of the notify reducer but i'm not sure this is good practice, and am not sure how to pull it off.
What is the best way to pull this off? I was thinking of adding an expiry date on each item but since the 'active' field is already there I would like to set a timeout and toggle it to false after 3 seconds..
Notification component:
export function Notification() {
const dispatch = useDispatch();
function disableAlert(id: number) {
dispatch(disable({'id' : id}));
}
const notification_list = useSelector(getNotification);
if (notification_list && notification_list.length > 0) {
return notification_list.map((notification: any, index: number) =>
notification.active ?
<Alert onClose={() => disableAlert(index)} style={{bottom: 50 * index}} severity={notification.mode}>{notification.message}</Alert> :
console.log(notification)
)
}
return <></>
}
Currently I have these slices:
const disableMessage = (state: any, message_id: number) => {
return state.messages.map((message:any) => message.id === message_id ?
{...message, active: !message.active} :
message
);
}
export const notificationSlice = createSlice({
name: 'notification',
initialState: initialState,
reducers: {
notify: (state, action) => {
const { message, mode, active } = action.payload;
state.messages.push({id: state.messages.length , message : message, mode: mode, active: active});
},
disable: (state, action) => {
const { id } = action.payload;
state.messages = disableMessage(state, id);
}
}
})

It is convention that reducers never contain any type of logic. I recommend to stick with this.
This leaves either the action or the Notification component. For me it makes more sense to tie the disable to the rendering of the individual notification so I would start the timeout there.
Ideally, you can split your <Alert/> component into the presentation and logic. Something similar to:
const NotificationAlert = ({ disableAlert, id }) => {
const notification = useSelector((state) => selectNotificationById(state, id));
const handleClick = useCallback(() => {
disableAlert(id);
}, [disableAlert, id]);
useEffect(() => {
setTimeout(() => disableAlert(id), 3000);
}, [disableAlert]);
return (
<Alert
onClose={handleClick}
style={{bottom: 50 * id}}
severity={notification.mode}>{notification.message}</Alert>
};
And
export function Notification() {
const dispatch = useDispatch();
// memoize handler with useCallback
const disableAlert = useCallback((id: number) => {
dispatch(disable({'id' : id}));
}, [dispatch]);
// Filter for active notifications already in your selector
const notificationIds = useSelector(getActiveNotificationIds);
return notificationIds.map((id) =>
<NotificationAlert disableAlert={disableAlert} id={id} />
);
}
Also, make sure your disableAlert action is setting active to false rather than toggling it!

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]);

Have to press twice to delete item in redux-toolkit

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

How to get updated redux-toolkit state when component is not re-render

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:
CODE UPDATED
export default function NoteList(props: noteListI) {
const { title, note, id, date } = props;
const data = useSelector((state: RootState) => state.persistedReducer.note);
useEffect(() => {
currentDate.current = data;
}, [data]);
const removeSelectedNote = () => {
dispatch(removeNote({ id: id }));
console.log(data); ====> still log 4 if i have 4
};
console.log(data); // ====> work if i log here but a lots of logs
return (
<View>
<TouchableOpacity
onLongPress={() => {
removeSelectedNote();
console.log("current", currentDate.current); ///same
}}
// 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, the redux state is change outside the return() but i can't capture the updated state, it work the previous one
Here is a gif to show what going on
When i press only one item, it change on UI but when i refresh it return same state
When i click twice or more, it make changes to previous
Updated
The redux-persist code:
const reducer = combineReducers({
note: noteReducer,
firebase: authentication,
});
const persistConfig = {
key: "root",
storage: AsyncStorage,
blacklist: [],
};
const persistedReducer = persistReducer(persistConfig, reducer);
const store = configureStore({
reducer: { persistedReducer, toggle: toggleReducer },
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: false,
}),
});
export default store;
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export const persistStorageNote = persistStore(store);
I also added the useEffect by this, but problem is when i log the changes in function, it remain the same:
here is how you can log updated data correctly, as state update is asynchronous it doesn’t change immediately when you dispatch removeNote
export default function NoteList(props: noteListI) {
const { title, note, id, date } = props;
const data = useSelector((state: RootState) => state.persistedReducer.note);
// log changed data
useEffect(() => {
console.log(data);
}, [data]);
const removeSelectedNote = () => {
dispatch(removeNote({ id: id }));
};
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>
);
}
about reloading issue, try to close the app and open it like a user of your app would (minimize the app -> remove the app from recently opened apps -> open app again ) , instead of reloading the project.

Translating context to redux with setTimeout

I have this context:
interface AlertContextProps {
show: (message: string, duration: number) => void;
}
export const AlertContext = createContext<AlertContextProps>({
show: (message: string, duration: number) => {
return;
},
});
export const AlertProvider: FC<IProps> = ({ children }: IProps) => {
const [alerts, setAlerts] = useState<JSX.Element[]>([]);
const show = (message: string, duration = 6000) => {
let alertKey = Math.random() * 100000;
setAlerts([...alerts, <Alert message={message} duration={duration} color={''} key={alertKey} />]);
setTimeout(() => {
setAlerts(alerts.filter((i) => i.key !== alertKey));
}, duration + 2000);
};
return (
<>
{alerts}
<AlertContext.Provider value={{ show }}>{children}</AlertContext.Provider>
</>
);
};
which I need to "translate" into a redux slice. I got a hang of everything, apart from the show method. What would be the correct way to treat it? I was thinking about a thunk, but it's not really a thunk. Making it a reducer with setTimeout also seems like an ugly thing to do. So how would you guys do it?
My code so far:
type Alert = [];
const initialState: Alert = [];
export const alertSlice = createSlice({
name: 'alert',
initialState,
reducers: {
setAlertState(state, { payload }: PayloadAction<Alert>) {
return payload;
},
},
});
export const { setAlertState } = alertSlice.actions;
export const alertReducer = alertSlice.reducer;
The timeout is a side effect so you could implement that in a thunk.
You have an action that shows an alert message that has a payload of message, id and time to display, when that time runs out then the alert message needs to be removed so you need a remove alert message action as well that is dispatched from the thunk with a payload of the id of the alert message.
I am not sure why you add 2 seconds to the time to hide the message duration + 2000 since the caller can decide how long the message should show I don't think it should half ignore that value and randomly add 2 seconds.
Here is a redux example of the alert message:
const { Provider, useDispatch, useSelector } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const initialState = {
messages: [],
};
//action types
const ADD_MESSAGE = 'ADD_MESSAGE';
const REMOVE_MESSAGE = 'REMOVE_MESSAGE';
//action creators
const addMessage = (id, text, time = 2000) => ({
type: ADD_MESSAGE,
payload: { id, text, time },
});
const removeMessage = (id) => ({
type: REMOVE_MESSAGE,
payload: id,
});
//id generating function
const getId = (
(id) => () =>
id++
)(1);
const addMessageThunk = (message, time) => (dispatch) => {
const id = getId();
dispatch(addMessage(id, message, time));
setTimeout(() => dispatch(removeMessage(id)), time);
};
const reducer = (state, { type, payload }) => {
if (type === ADD_MESSAGE) {
return {
...state,
messages: state.messages.concat(payload),
};
}
if (type === REMOVE_MESSAGE) {
return {
...state,
messages: state.messages.filter(
({ id }) => id !== payload
),
};
}
return state;
};
//selectors
const selectMessages = (state) => state.messages;
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
initialState,
composeEnhancers(
applyMiddleware(
//simple implementation of thunk (not official redux-thunk)
({ dispatch }) =>
(next) =>
(action) =>
typeof action === 'function'
? action(dispatch)
: next(action)
)
)
);
const App = () => {
const messages = useSelector(selectMessages);
const dispatch = useDispatch();
return (
<div>
<button
onClick={() =>
dispatch(addMessageThunk('hello world', 1000))
}
>
Add message
</button>
<ul>
{messages.map((message) => (
<li key={message.id}>{message.text}</li>
))}
</ul>
</div>
);
};
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<div id="root"></div>
#HMR's use of a thunk is fine, but I don't like what they've done to your reducer. You're already using redux-toolkit which is great! redux-toolkit actually includes and exports a nanoid function which they use behind the scenes to create unique ids for thunks. You can use that instead of Math.random() * 100000.
I always start by thinking about types. What is an Alert? You don't want to store the <Alert/> because a JSX.Element is not serializable. Instead you should just store the props. You'll definitely store the message and key/id. If you handle expiration on the front-end then you would also store the duration, but if the expiration is handled by a thunk then I don't think you need it in the redux state or component props.
It seems like you want to allow multiple alerts at one time, so return payload is not going to cut it for your reducer. You'll need to store an array or a keyed object will all of your active alerts.
You absolute should not use setTimeout in a reducer because that is a side effect. You can use it either in a thunk or in a useEffect in the Alert component. My inclination is towards the component because it seems like the alert should probably be dismissible as well? So you can use the same function for handling dismiss clicks and automated timeouts.
We can define the info that we want to store for each alert.
type AlertData = {
message: string;
id: string;
duration: number;
}
And the info that we need to create that alert, which is the same but without the id because we will generate the id in the reducer.
type AlertPayload = Omit<AlertData, 'id'>
Our state can be an array of alerts:
const initialState: AlertData[] = [];
We need actions to add a new alert and to remove an alert once it has expired.
import { createSlice, PayloadAction, nanoid } from "#reduxjs/toolkit";
...
export const alertSlice = createSlice({
name: "alert",
initialState,
reducers: {
addAlert: (state, { payload }: PayloadAction<AlertPayload>) => {
const id = nanoid(); // create unique id
state.push({ ...payload, id }); // add to the state
},
removeAlert: (state, { payload }: PayloadAction<string>) => {
// filter the array -- payload is the id
return state.filter((alert) => alert.id !== payload);
}
}
});
export const { addAlert, removeAlert } = alertSlice.actions;
export const alertReducer = alertSlice.reducer;
So now to the components. What I have in mind is that you would use a selector to select all of the alerts and then each alert will be responsible for its own expiration.
export const AlertComponent = ({ message, duration, id }: AlertData) => {
const dispatch = useDispatch();
// function called when dismissed, either by click or by timeout
// useCallback is just so this can be a useEffect dependency and won't get recreated
const remove = useCallback(() => {
dispatch(removeAlert(id));
}, [dispatch, id]);
// automatically expire after the duration, or if this component unmounts
useEffect(() => {
setTimeout(remove, duration);
return remove;
}, [remove, duration]);
return (
<Alert
onClose={remove} // can call remove directly by clicking the X
dismissible
>
<Alert.Heading>Alert!</Alert.Heading>
<p>{message}</p>
</Alert>
);
};
export const ActiveAlerts = () => {
const alerts = useSelector((state) => state.alerts);
return (
<>
{alerts.map((props) => (
<AlertComponent {...props} key={props.id} />
))}
</>
);
};
I also made a component to create alerts to test this out and make sure that it works!
export const AlertCreator = () => {
const dispatch = useDispatch();
const [message, setMessage] = useState("");
const [duration, setDuration] = useState(8000);
return (
<div>
<h1>Create Alert</h1>
<label>
Message
<input
type="text"
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
</label>
<label>
Duration
<input
type="number"
step="1000"
value={duration}
onChange={(e) => setDuration(parseInt(e.target.value, 10))}
/>
</label>
<button
onClick={() => {
dispatch(addAlert({ message, duration }));
setMessage("");
}}
>
Create
</button>
</div>
);
};
const App = () => (
<div>
<AlertCreator />
<ActiveAlerts />
</div>
);
export default App;
Code Sandbox Link

How to make dispatch call synchronous in useeffect?

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

Categories

Resources