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;
}
Related
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.
I have a few functional components with a few common variables and functions. Please see below.
const Customer = () => {
const [isReleased, setIsReleased] = useState(false)
const release = () => {
setIsReleased(true)
}
}
const Order = () => {
const [isReleased, setIsReleased] = useState(false)
const release = () => {
setIsReleased(true)
}
}
As you can see from the above code fragment, the release() function has common logic. It accesses component's variables/functions.
Is there a way to move this release() function to a common file and import it from each component?
Please note that the release() method should be able to access scoped variables and functions of the caller.
Update
Below is the actual content of the release() function. I have put this. to denote that it refers to the variables/functions in the caller.
const release = () => {
if (action === "new") {
history.push(`/customers/new`)
} else if (action === "save") {
(async () => {
try {
if (this.dataMode === "new") {
this.setMessage()
this.setFormStatus("updating")
let _res = await this.customer_api_create(this.formData)
if ((_res.status === 200) && (_res.data.status === "success")) {
this.setFormStatus()
history.replace({ pathname: `/customers/${_res.data.data[0].id}` })
}
} else if (this.dataMode === "edit") {
this.setFormStatus("updating")
this.setMessage()
let _res = await this.customer_api_update(this.formData)
if ((_res.status === 200) && (_res.data.status === "success")) {
this.setFormStatus()
this.setMessage({ type: "info", text: "Saved" })
this.setFormData(_res.data.data[0])
}
}
} catch (e) {
this.openMessageBox({
prompt: e.response.data.message,
type: this.constants.app.MessageBoxType.MB_Error,
buttons: this.constants.app.MessageBoxButton.MB_Ok,
show: true,
setResult: () => console.log(this.constants.app.MessageBoxResult.MB_Ok)
})
this.setFormStatus("updating_error")
}
})();
} else if (action === "del") {
this.openMessageBox({
prompt: `{ "": ["Are you sure you want to delete context customer?"] }`,
type: this.constants.app.MessageBoxType.MB_Warning,
buttons: this.constants.app.MessageBoxButton.MB_YesNo,
show: true,
setResult: (val) => {
this.openMessageBox({ show: false })
this.setShouldRecordDeleted(val)
}
})
}
}
You can declare the function release in a different file and pass the setIsReleased method as an argument
import {release} from 'release';
const Customer = () => {
const [isReleased, setIsReleased] = useState(false)
release(setIsReleased)
}
And in your release.js file
export const release = (setIsReleased) => {
setIsReleased(true)
}
Maybe callback solve the problem, how about this?
anotherfile.js
const release = (param, setter) => {
if (true){
setter(true)
}
}
yourfile.js
import {release} from "anotherfile"
const Customer = () => {
const [isReleased, setIsReleased] = useState(false)
release([parameter],setIsReleased)
}
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 want login system. My export default not working Read function. Read function query user_id in AsyncStorage. Please help me :)
app.js
var sendTabs = <TabsScreens />
var sendLogin = <LoginScreens />
var Read = readStore = async () => {
try {
const value = await AsyncStorage.getItem('user_id');
if (value !== null) {
return 1;
} else {
return 0;
}
} catch (e) {
alert(e);
}
}
var Welcome;
Read().then((response) => {
if (response == 1) {
Welcome = sendTabs
} else {
Welcome = sendLogin;
}
});
export default () => (Welcome)
You could define new component to handle this logic:
export function WelcomeScreen() {
const [isLoggedIn, setIsLoggedIn] = React.useState(null); // null | true | false
React.useEffect(() => {
void async function getUserId() {
const id = await AsyncStorage.getItem('user_id');
setIsLoggedIn(Boolean(id)); // true -> user_id found, false -> no valid user_id found
}();
}, []);
return (userId === null)
? <Text>Loading...</Text> // this will show until storage is checked
: (isLoggedIn) ? <TabsScreens /> : <LoginScreens />; // depending on the value of id from storage you show tabs or login screen
}
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