use async function to get draft inside reducer of useImmerReducer - javascript

I have this reducer function that I use for state management of my app.
const initialState = {roles: null};
const reducer = (draft, action) => {
switch (action.type) {
case 'initialize':
//what should i do here????
return;
case 'add':
draft.roles = {...draft.roles, action.role};
return;
case 'remove':
draft.roles = Object.filter(draft.roles, role => role.name != action.role.name);
}
};
const [state, dispatch] = useImmerReducer(reducer, initialState);
to initialize my state I must use an async function that reads something from asyncStorage if it exists, must set draft.roles to it, if not it should be set to a default value.
const initialize = async () => {
try {
let temp = await cache.get();
if (temp == null) {
return defaultRoles;
} else {
return temp;
}
} catch (error) {
console.log('initialization Error: ', error);
return defaultRoles;
}
};
how can I get initilize function returned value inside 'initialize' case? if I use initilize().then(value=>draft.roles=value) I get this error:
TypeError: Proxy has already been revoked. No more operations are allowed to be performed on it

You cannot use asynchronous code inside of a reducer. You need to move that logic outside of the reducer itself. I am using a useEffect hook to trigger the initialize and then dispatching the results to the state.
There are quite a few syntax errors here -- should state.roles be an array or an object?
Here's my attempt to demonstrate how you can do this. Probably you want this as a Context Provider component rather than a hook but the logic is the same.
Javascript:
import { useEffect } from "react";
import { useImmerReducer } from "use-immer";
export const usePersistedReducer = () => {
const initialState = { roles: [], didInitialize: false };
const reducer = (draft, action) => {
switch (action.type) {
case "initialize":
// store all roles & flag as initialized
draft.roles = action.roles;
draft.didInitialize = true;
return;
case "add":
// add one role to the array
draft.roles.push(action.role);
return;
case "remove":
// remove role from the array based on name
draft.roles = draft.roles.filter(
(role) => role.name !== action.role.name
);
return;
}
};
const [state, dispatch] = useImmerReducer(reducer, initialState);
useEffect(() => {
const defaultRoles = []; // ?? where does this come from?
// always returns an array of roles
const retrieveRoles = async () => {
try {
// does this need to be deserialized?
let temp = await cache.get();
// do you want to throw an error if null?
return temp === null ? defaultRoles : temp;
} catch (error) {
console.log("initialization Error: ", error);
return defaultRoles;
}
};
// define the function
const initialize = async() => {
// wait for the roles
const roles = await retrieveRoles();
// then dispatch
dispatch({type: 'initialize', roles});
}
// execute the function
initialize();
}, [dispatch]); // run once on mount - dispatch should not change
// should use another useEffect to push changes
useEffect(() => {
cache.set(state.roles);
}, [state.roles]); // run whenever roles changes
// maybe this should be a context provider instead of a hook
// but this is just an example
return [state, dispatch];
};
Typescript:
import { Draft } from "immer";
import { useEffect } from "react";
import { useImmerReducer } from "use-immer";
interface Role {
name: string;
}
interface State {
roles: Role[];
didInitialize: boolean;
}
type Action =
| {
type: "initialize";
roles: Role[];
}
| {
type: "add" | "remove";
role: Role;
};
// placeholder for the actual
declare const cache: { get(): Role[] | null; set(v: Role[]): void };
export const usePersistedReducer = () => {
const initialState: State = { roles: [], didInitialize: false };
const reducer = (draft: Draft<State>, action: Action) => {
switch (action.type) {
case "initialize":
// store all roles & flag as initialized
draft.roles = action.roles;
draft.didInitialize = true;
return;
case "add":
// add one role to the array
draft.roles.push(action.role);
return;
case "remove":
// remove role from the array based on name
draft.roles = draft.roles.filter(
(role) => role.name !== action.role.name
);
return;
}
};
const [state, dispatch] = useImmerReducer(reducer, initialState);
useEffect(() => {
const defaultRoles: Role[] = []; // ?? where does this come from?
// always returns an array of roles
const retrieveRoles = async () => {
try {
// does this need to be deserialized?
let temp = await cache.get();
// do you want to throw an error if null?
return temp === null ? defaultRoles : temp;
} catch (error) {
console.log("initialization Error: ", error);
return defaultRoles;
}
};
// define the function
const initialize = async() => {
// wait for the roles
const roles = await retrieveRoles();
// then dispatch
dispatch({type: 'initialize', roles});
}
// execute the function
initialize();
}, [dispatch]); // run once on mount - dispatch should not change
// should use another useEffect to push changes
useEffect(() => {
cache.set(state.roles);
}, [state.roles]); // run whenever roles changes
// maybe this should be a context provider instead of a hook
// but this is just an example
return [state, dispatch];
};

Related

Redux createAsyncThunk payload showing undfined

This is my jsonApiFetchOiAndLTPs reducer
export const jsonApiFetchOiAndLTPs = createAsyncThunk('exchange/jsonApiFetchOiAndLTPs', async (payload, { dispatch, getState }) => {
const { account, exchange, filters, registry } = getState()
const appliedFilters = payload === undefined ? filters : payload?.filters
console.log('payload', payload); // here payload always being undefined
console.log('appliedFilters', appliedFilters);
// remaining code goes here
}
In one of my file i am dispatching action as shwon below
useEffect(() => {
if(open) {
localStorageData?.forEach(item => {
const specKey = contractSpecKey(item)
if(!contracts.hasOwnProperty(specKey)) {
const appliedFilters = {
tokenPair: item.tokenPair,
optionStyle: item.optionStyle,
contractType: item.contractType,
expiryDate: item.expiryDate,
lotSize: item.lotSize
}
dispatch(jsonApiFetchOiAndLTPs({filters: appliedFilters}))
// here i am dispatching the action
}
})
}
}, [open])
For each items of localstorage data i am dispatching action based on if consition

Already populated array in useContext giving me an empty array when accessing it in a function inside of the same context?

The problem I'm having is, that I have a useContext in which I provide all logged users. On the initial run of the app or when the users' log in the array gets populated with all the users that are currently on the server... Which works as expected. But I have also the functionality, that whenever the server "user-connected" event runs, the front-end should just push the user to the end of this array. And there lays the problem. From the backend, the right user is sent, but when I access the connectedUsers array, the array is empty... but it should be already populated.
UsersProvider.tsx
export const inState = {
connectedUsers: [],
addUser: (user: any) => {},
sortUsers: (user: any, socketID: string) => {},
setLoggedUsers: () => {},
};
export interface initState {
connectedUsers: any[];
addUser(user: any): void;
sortUsers(users: any, socketID: string): void;
setLoggedUsers: React.Dispatch < React.SetStateAction < any[] >> ;
}
const UsersContext = createContext < initState > (inState);
export const useUsers = () => {
return useContext(UsersContext);
};
const initUserProps = (user: any) => {
user.messages = [];
user.hasNewMessages = false;
};
export const UsersProvider = ({
children
}: Props) => {
const [connectedUsers, setLoggedUsers] = useState < any[] > ([]);
const addUser = (user: any) => {
console.log('add', connectedUsers);
// This is empty, but it should be already populated when next user connected.
};
const sortUsers = (users: any, socketUserID: string) => {
const usersCopy = users;
usersCopy.forEach((u: any) => {
for (let i = 0; i < usersCopy.length; i++) {
const existingUser = usersCopy[i];
if (existingUser.userID === u.userID) {
existingUser.connected = u.connected;
break;
}
}
u.self = u.userID === socketUserID;
initUserProps(u);
});
// put the current user first, and sort by username
let sorted = usersCopy.sort((a: any, b: any) => {
if (a.self) return -1;
if (b.self) return 1;
if (a.username < b.username) return -1;
return a.username > b.username ? 1 : 0;
});
setLoggedUsers([...sorted]);
};
return ( <
UsersContext.Provider value = {
{
connectedUsers,
setLoggedUsers,
addUser,
sortUsers
}
} >
{
children
} <
/UsersContext.Provider>
);
};
And the part of ChatBoard.tsx, you can find addUser function initiated whenever user-connected happens. I really don't know why the would array be empty, if it is populated on the first run with users event.
const ChatBoard = (props: Props) => {
const socket = useSocket();
const {
connectedUsers,
setLoggedUsers,
addUser,
sortUsers
} = useUsers();
useEffect(() => {
if (socket == null) return;
socket.on('users', (users) => {
console.log(users);
if (socket.userID) {
const socketID: string = socket ? .userID;
sortUsers(users, socketID);
}
});
socket.on('user-connected', (user: any) => {
console.log(user, 'this user connected!');
const connectingUser = user;
addUser(connectingUser);
});
socket.on('user-disconnected', (userID) => {
console.log('disconnected user');
const users = [...connectedUsers];
users.forEach((u) => {
if (u.userID === userID) {
u.connected = false;
setLoggedUsers([...users]);
}
});
});
return () => {
socket.off('users');
socket.off('user-connected');
};
}, [socket]);
CodeSandbox
So I have found the problem... so with React hooks sometimes a problem occurs called "Stale Closures", which means that React was picking up the old state (empty one, the one that was not yet populated and always returning that one.).
The solution to this problem, in my case is that when you use setState you use it with a callback. Like so, so you always get the latest state.
const addUser = (user: any) => {
setLoggedUsers((oldUsers) => {
const newUsers: any[] = [...oldUsers];
console.log(newUsers);
for (let i = 0; i < newUsers.length; i++) {
const existingUser = newUsers[i];
if (existingUser.userID === user.userID) {
existingUser.connected = true;
return newUsers;
}
}
initReactiveProperties(user);
newUsers.push(user);
return newUsers;
});
};

Rendered fewer hooks than expected. This may be caused by an accidental early return statement

I'm getting this error when triggering a setState inside of a custom React hook. I'm not sure of how to fix it, can anyone show me what I'm doing wrong. It is getting the error when it hits handleSetReportState() line. How should I be setting the report state from inside the hook?
custom useinterval poll hook
export function usePoll(callback: IntervalFunction, delay: number) {
const savedCallback = useRef<IntervalFunction | null>()
useEffect(() => {
savedCallback.current = callback
}, [callback])
useEffect(() => {
function tick() {
if (savedCallback.current !== null) {
savedCallback.current()
}
}
const id = setInterval(tick, delay)
return () => clearInterval(id)
}, [delay])
}
React FC
const BankLink: React.FC = ({ report: _report }) => {
const [report, setReport] = React.useState(_report)
if ([...Statues].includes(report.status)) {
usePoll(async () => {
const initialStatus = _report.status
const { result } = await apiPost(`/links/search` });
const currentReport = result.results.filter((item: { id: string; }) => item.id === _report.id)
if (currentReport[0].status !== initialStatus) {
handleSetReportState(currentReport[0])
console.log('status changed')
} else {
console.log('status unchanged')
}
}, 5000)
}
... rest
This is because you put usePoll in if condition, see https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
You can put the condition into the callback
usePoll(async () => {
if ([...Statues].includes(report.status)) {
const initialStatus = _report.status
const { result } = await apiPost(`/links/search` });
const currentReport = result.results.filter((item: { id: string; }) => item.id === _report.id)
if (currentReport[0].status !== initialStatus) {
handleSetReportState(currentReport[0])
console.log('status changed')
} else {
console.log('status unchanged')
}
}
}, 5000)
And if the delay will affect report.status, use ref to store report.status and read from ref value in the callback.

Can't use new redux state right after fetching a response from Socket.IO

I have a function "sendMessage" in React class:
class MessageForm extends React.Component {
...
sendMessage = async () => {
const { message } = this.state;
if (message) {
this.setState({ loading: true });
if (this.props.isPrivateChannel === false) {
socket.emit("createMessage", this.createMessage(), (response) => {
this.setState({ loading: false, message: "", errors: [] });
});
} else {
if (this.state.channel && this.state.channel._id === undefined) {
socket.emit("createChannelPM", this.state.channel, async (response) => {
const chInfo = { ...response, name: this.props.currentChannel.name };
console.log("chInfo : ", chInfo);
await this.props.setCurrentChannel(chInfo).then((data) => {
if (data) {
console.log("data : ", data);
console.log("this.props.currentChannel : ", this.props.currentChannel);
}
});
});
}
...
function mapStateToProps(state) {
return {
isPrivateChannel: state.channel.isPrivateChannel,
currentChannel: state.channel.currentChannel,
};
}
const mapDispatchToProps = (dispatch) => {
return {
setCurrentChannel: async (channel) => await dispatch(setCurrentChannel(channel)),
}
};
Here, in sendMessage function, I retrieve "response" from socket.io, then put this data into variable "chInfo" and assign this to Redux state, then print it right after assinging it.
And Redux Action function, "setCurrentChannel" looks like:
export const setCurrentChannel = channel => {
return {
type: SET_CURRENT_CHANNEL,
payload: {
currentChannel: channel
}
};
};
Reducer "SET_CURRENT_CHANNEL" looks like:
export default function (state = initialState, action) {
switch (action.type) {
case SET_CURRENT_CHANNEL:
return {
...state,
currentChannel: action.payload.currentChannel
};
...
The backend Socket.io part look like (I use MongoDB):
socket.on('createChannelPM', async (data, callback) => {
const channel = await PrivateChannel.create({
...data
});
callback(channel)
});
The console.log says:
Problem : The last output, "this.props.currentChannel" should be same as the first output "chInfo", but it is different and only print out previous value.
However, in Redux chrome extension, "this.props.currentChannel" is exactly same as "chInfo":
How can I get and use newly changed Redux states immediately after assinging it to Redux State?
You won't get the updated values immediately in this.props.currentChannel. After the redux store is updated mapStateToProps of MessageForm component is called again. Here the state state.channel.currentChannel will be mapped to currentChannel. In this component you get the updated props which will be accessed as this.props.currentChannel.
I believe you want to render UI with the latest data which you which you can do.

Removing items older than x from localStorage (Redux)

I have a state in the reducer, that is built in the following way:
state = {
companies: ['company1', 'company2', 'company3', 'company4']
}
These companies are fetched from API and added to the state.
And I would like to remove from localStorage only the companies which were added to the state x hours before or earlier. How should I properly approach this task?
My localStorage.js:
export const loadState = () => {
try {
const serializedState = localStorage.getItem('state')
if (serializedState === null) {
return undefined;
}
return JSON.parse(serializedState)
} catch (err) {
return undefined
}
}
export const saveState = (state) => {
try {
const serializedState = JSON.stringify(state)
localStorage.setItem('state', serializedState)
} catch (err) {
// to define
}
}
saving in state happens in redcuer's CASE that looks like this:
case FETCH_FROM_API:
return {
...state,
companies: [payload, ...state.companies]
};

Categories