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]
};
Related
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
I have a question about some weird behavior when using React state.
If I change the default type of the groupedFormErrors variable in the helper function to [] instead of {} the state in the page element formErrors doesn't update. Would be great if somebody could explain why this happens.
Page element
const [formErrors, setFormErrors] = useState();
const signUpHandler = async (e) => {
e.preventDefault();
const response = await submitForm();
if (response.status === 422) {
setFormErrors(groupFormErrors(response));
}
};
Helper function
export default function GroupFormErrors(response) {
let groupedFormErrors = {}; // <<<<<<<<<<<<<<<<<<<<< This here
if (response.status === 422) {
response.data.forEach((error) => {
if (groupedFormErrors.hasOwnProperty(error.param)) {
groupedFormErrors[error.param].push(error.msg);
} else {
groupedFormErrors[error.param] = [];
groupedFormErrors[error.param].push(error.msg);
}
});
}
return groupedFormErrors;
}
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];
};
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.
I started integrating websockets into an existing React/Django app following along with this example (accompanying repo here). In that repo, the websocket interface is in websockets.js, and is implemented in containers/Chat.js.
I can get that code working correctly as-is.
I then started re-writing my implementation to use Hooks, and hit a little wall. The data flows through the socket correctly, arrives in the handler of each client correctly, and within the handler can read the correct state. Within that handler, I'm calling my useState function to update state with the incoming data.
Originally I had a problem of my single useState function within addMessage() inconsistently firing (1 in 10 times?). I split my one useState hook into two (one for current message, one for all messages). Now in addMessage() upon receiving data from the server, my setAllMessages hook will only update the client where I type the message in - no other clients. All clients receive/can log the data correctly, they just don't run the setAllMessages function.
If I push to an empty array outside the function, it works as expected. So it seems like a problem in the function update cycle, but I haven't been able to track it down.
Here's my version of websocket.js:
class WebSocketService {
static instance = null;
static getInstance() {
if (!WebSocketService.instance) {
WebSocketService.instance = new WebSocketService();
}
return WebSocketService.instance;
}
constructor() {
this.socketRef = null;
this.callbacks = {};
}
disconnect() {
this.socketRef.close();
}
connect(chatUrl) {
const path = `${URLS.SOCKET.BASE}${URLS.SOCKET.TEST}`;
this.socketRef = new WebSocket(path);
this.socketRef.onopen = () => {
console.log('WebSocket open');
};
this.socketRef.onmessage = e => {
this.socketNewMessage(e.data);
};
this.socketRef.onerror = e => {
console.log(e.message);
};
this.socketRef.onclose = () => {
this.connect();
};
}
socketNewMessage(data) {
const parsedData = JSON.parse(data);
const { command } = parsedData;
if (Object.keys(this.callbacks).length === 0) {
return;
}
Object.keys(SOCKET_COMMANDS).forEach(clientCommand => {
if (command === SOCKET_COMMANDS[clientCommand]) {
this.callbacks[command](parsedData.presentation);
}
});
}
backend_receive_data_then_post_new(message) {
this.sendMessage({
command_for_backend: 'backend_receive_data_then_post_new',
message: message.content,
from: message.from,
});
}
sendMessage(data) {
try {
this.socketRef.send(JSON.stringify({ ...data }));
} catch (err) {
console.log(err.message);
}
}
addCallbacks(allCallbacks) {
Object.keys(SOCKET_COMMANDS).forEach(command => {
this.callbacks[SOCKET_COMMANDS[command]] = allCallbacks;
});
}
state() {
return this.socketRef.readyState;
}
}
const WebSocketInstance = WebSocketService.getInstance();
export default WebSocketInstance;
And here's my version of Chat.js
export function Chat() {
const [allMessages, setAllMessages] = useState([]);
const [currMessage, setCurrMessage] = useState('');
function waitForSocketConnection(callback) {
setTimeout(() => {
if (WebSocketInstance.state() === 1) {
callback();
} else {
waitForSocketConnection(callback);
}
}, 100);
}
waitForSocketConnection(() => {
const allCallbacks = [addMessage];
allCallbacks.forEach(callback => {
WebSocketInstance.addCallbacks(callback);
});
});
/*
* This is the problem area
* `incoming` shows the correct data, and I have access to all state
* But `setAllMessages` only updates on the client I type the message into
*/
const addMessage = (incoming) => {
setAllMessages([incoming]);
};
// update with value from input
const messageChangeHandler = e => {
setCurrMessage(e.target.value);
};
// Send data to socket interface, then to server
const sendMessageHandler = e => {
e.preventDefault();
const messageObject = {
from: 'user',
content: currMessage,
};
setCurrMessage('');
WebSocketInstance.backend_receive_data_then_post_new(messageObject);
};
return (
<div>
// rendering stuff here
</div>
);
}
There is no need to rewrite everything into functional components with hooks.
You should decompose it functionally - main (parent, class/FC) for initialization and providing [data and] methods (as props) to 2 functional childrens/components responsible for rendering list and input (new message).
If you still need it ... useEffect is a key ... as all code is run on every render in functional components ... including function definitions, redefinitions, new refs, duplications in callbacks array etc.
You can try to move all once defined functions into useEffect
useEffect(() => {
const waitForSocketConnection = (callback) => {
...
}
const addMessage = (incoming) => {
setAllMessages([incoming]);
};
waitForSocketConnection(() => {
...
}
}, [] ); // <<< RUN ONCE