Retrieving state from React Router v6 Navigate - reactjs

I'm trying to pass state data after navigating to a new route and typescript is telling me
Property 'email' does not exist on type 'State'."
Parent functional component:
navigate('/check-mail', { state: { email: "hello, I'm an email" } });
Child functional Component:
const SentPasswordResetInstructions = () => {
const location = useLocation();
let { email } = location.state;
}
I've tried creating an interface like so:
interface propState { email : string }
and then using
useLocation<propState>();
However that throws additional errors. How do I fix this ??

Just solved it! Creating the interface:
interface propState {
email: string;
}
And then using
let { email } = location.state as propState;
Worked!

useLocation().state is undefined by default, such as when you navigate directly to this route. Because of this, destructuring properties from it can throw an error because you'll essentially be doing
const { email } = undefined;
I recommend using some form of type guard or validator (such as Zod) for the state, so that you can handle situations where it might not contain any data.
type MyState = { email: string }
function isStateValid(state: any): state is MyState {
if (!state) return false; // Makes sure it's not null
if (typeof state !== "object") return false;
if (typeof state.email !== "string") return false;
return true;
}
const { state } = useLocation(); // state is any or unknown
if (isStateValid(state)) {
const { email } = state; // state is MyState here
}

Related

React Custom Hook useLocalStorage gives Error "Cannot read properties of null (reading 'useCallback')"

I am really greatful If any one can help to resolve this issue.
Can I Call react hooks in any function inside typescript file?
React Hook useLocalStorage gives error when using
const [access_token] = useLocalStorage("access_token", null);
Below is the code
import {
Dispatch,
SetStateAction,
useCallback,
useEffect,
useState,
} from "react";
import { useEventCallback } from "./useEventCallback";
// See: https://usehooks-ts.com/react-hook/use-event-listener
import { useEventListener } from "./useEventListener";
declare global {
interface WindowEventMap {
"local-storage": CustomEvent;
}
}
type SetValue<T> = Dispatch<SetStateAction<T>>;
export function useLocalStorage<T>(
key: string,
initialValue: T
): [T | null, SetValue<T | null>] {
// Get from local storage then
// parse stored json or return initialValue
const readValue = useCallback((): T | null => {
// Prevent build error "window is undefined" but keep keep working
if (typeof window === "undefined") {
return initialValue;
}
try {
const item = window.localStorage.getItem(key);
return item ? (parseJSON(item) as T) : initialValue;
} catch (error) {
console.warn(`Error reading localStorage key “${key}”:`, error);
return initialValue;
}
}, [initialValue, key]);
// State to store our value
// Pass initial state function to useState so logic is only executed once
const [storedValue, setStoredValue] = useState<T | null>(readValue);
// Return a wrapped version of useState's setter function that ...
// ... persists the new value to localStorage.
const setValue: SetValue<T | null> = useEventCallback((value) => {
// Prevent build error "window is undefined" but keeps working
if (typeof window == "undefined") {
console.warn(
`Tried setting localStorage key “${key}” even though environment is not a client`
);
}
try {
// Allow value to be a function so we have the same API as useState
const newValue = value instanceof Function ? value(storedValue) : value;
// Save to local storage
window.localStorage.setItem(key, JSON.stringify(newValue));
// Save state
setStoredValue(newValue);
// We dispatch a custom event so every useLocalStorage hook are notified
window.dispatchEvent(new Event("local-storage"));
} catch (error) {
console.warn(`Error setting localStorage key “${key}”:`, error);
}
});
useEffect(() => {
setStoredValue(readValue());
// eslint-disable-next-line
}, []);
const handleStorageChange = useCallback(
(event: StorageEvent | CustomEvent) => {
if ((event as StorageEvent)?.key && (event as StorageEvent).key !== key) {
return;
}
setStoredValue(readValue());
},
[key, readValue]
);
// this only works for other documents, not the current one
useEventListener("storage", handleStorageChange);
// this is a custom event, triggered in writeValueToLocalStorage
// See: useLocalStorage()
useEventListener("local-storage", handleStorageChange);
return [storedValue, setValue];
}
// A wrapper for "JSON.parse()"" to support "undefined" value
function parseJSON<T>(value: string | null): T | undefined {
try {
return value === "undefined" ? undefined : JSON.parse(value ?? "");
} catch {
console.log("parsing error on", { value });
return undefined;
}
}

React - Error: Maximum update depth exceeded - useReducer

I'm facing an error:
react-dom.development.js:23093 Uncaught Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
I understand that the problem may be due to the fact that I'm calling the checkError and validationPassed functions that modify the state through the useReducer, within the checkValidations function that is called through the useEffect hook, but I don't know how to solve it
The code is as:
interface ValidationField {
errorMessage?: string;
focused: boolean;
hasError: boolean;
}
interface ClientEditorState {
client: Client;
validations: { [key in keyof Client]: ValidationField };
}
enum clientEditorActions {
UPDATE_ENTITY = 'CLIENT_EDITOR/UPDATE_ENTITY',
UPDATE_FOCUSED = 'CLIENT_EDITOR/UPDATE_FOCUSED',
VALIDATION_ERROR = 'CLIENT_EDITOR/VALIDATION_ERROR',
VALIDATION_PASSED = 'CLIENT_EDITOR/VALIDATION_PASSED',
}
interface UpdateEntityAction extends Action<typeof clientEditorActions.UPDATE_ENTITY> {
name: string;
value: string | boolean;
}
interface UpdateFocusedAction extends Action<typeof clientEditorActions.UPDATE_FOCUSED> {
name: string;
}
interface ValidationErrorAction extends Action<typeof clientEditorActions.VALIDATION_ERROR> {
message: string;
name: string;
}
interface ValidationPassedAction extends Action<typeof clientEditorActions.VALIDATION_PASSED> {
name: string;
}
type ClientEditorActions = UpdateEntityAction | UpdateFocusedAction | ValidationErrorAction | ValidationPassedAction;
const clientReducer: Reducer<ClientEditorState, ClientEditorActions> = (prevState, action) => {
switch (action.type) {
case clientEditorActions.UPDATE_ENTITY:
const clientUpdated = _cloneDeep(prevState || ({} as Client));
_set(clientUpdated, `client.${action.name}`, action.value);
return clientUpdated;
case clientEditorActions.UPDATE_FOCUSED:
const validationField = _cloneDeep(prevState);
_set(validationField, `validations.${action.name}.focused`, true);
return validationField;
case clientEditorActions.VALIDATION_ERROR:
const errorField = _cloneDeep(prevState);
_set(errorField, `validations.${action.name}.hasError`, true);
_set(errorField, `validations.${action.name}.errorMessage`, action.message);
return errorField;
case clientEditorActions.VALIDATION_PASSED:
const passed = _cloneDeep(prevState);
_set(passed, `validations.${action.name}.hasError`, false);
_set(passed, `validations.${action.name}.errorMessage`, undefined);
return passed;
default:
return prevState;
}
};
...
const getInitialState = (): ClientEditorState => ({
client: entity as Client,
validations: {
firstName: {
focused: false,
hasError: false,
},
},
});
const [state, clientDispatch] = useReducer(clientReducer, getInitialState());
const checkError = useCallback((name: string, message: string) => {
clientDispatch({
type: clientEditorActions.VALIDATION_ERROR,
name,
message,
});
}, []);
const validationPassed = useCallback((name: string) => {
clientDispatch({
type: clientEditorActions.VALIDATION_PASSED,
name,
});
}, []);
const checkValidations = useCallback(
(c: Client) => {
let validation = false;
const { firstName } = state.validations;
if (!c.firstName && firstName.focused) {
validation = false;
checkError('firstName', f('client.requiredFieldClient'));
} else {
validation = true;
validationPassed('firstName');
}
},
[checkError, f, state.validations, validationPassed],
);
const [clientUpdateHandler] = useDebouncedCallback((clientUpdated: Client) => {
dispatch(updateEntityEditor(clientUpdated));
}, 800);
useEffect(() => {
if (!_isEqual(state.client, entity)) {
clientUpdateHandler(state.client as Client);
}
const { firstName } = state.validations;
if (firstName.focused) checkValidations(state.client);
}, [checkValidations, clientUpdateHandler, entity, state.client, state.validations]);
I understand that the problem may be due to the fact that I'm calling the checkError and validationPassed functions that modify the state through the useReducer, within the checkValidations function that is called through the useEffect hook
Yeah, exactly. Try to reduce your dependencies in the useEffect and the associated functions in useCallback. Any one of these changing will cause the useEffect to rerun.
Back to the question, your useEffect currently depends on state.validations. So whenever state.validations changes the useEffect will rerun. Consider doing
const { firstName } = state.validations;
outside the useEffect and the checkValidations callback. This will stop it from rerunning every time state.validations changes.

Passing data from one screen to another screen with react-navigation

I'm currently passing the data to one component in react-native with some code which is shown as below:
class Login extends Component {
signInWithGoogle = async () => {
try {
const result = await Google.logInAsync({
iosClientId: IOS_CLIENT_ID,
androidClientId: ANDROID_CLIENT_ID,
scopes: ["profile", "email"]
});
if (result.type === "success") {
console.log(
"LoginScreen.js.js 21 | ",
result.user.givenName,
result.user.familyName,
result.user.email,
result.user.photoUrl
);
this.props.navigation.navigate(
"MyDrawer",
(username = result.user.givenName),
(lastname = result.user.familyName),
(email = result.user.email),
(photoUrl = result.user.photoUrl)
);
return result.accessToken;
} else {
return { cancelled: true };
}
} catch (e) {
console.log("LoginScreen.js.js 30 | Error with login", e);
return { error: true };
}
};
}
My question is how do I pass the data to a different component?
you need to pass an object as second parameter to the navigate method that is in props.
to pass a data to a different component, provide an object as the second argument for the navigate function
change this
this.props.navigation.navigate(
"MyDrawer",
(username = result.user.givenName),
(lastname = result.user.familyName),
(email = result.user.email),
(photoUrl = result.user.photoUrl)
);
to
this.props.navigation.navigate("MyDrawer",{
name: result.user.givenName,
lastname : result.user.familyName,
email :result.user.email,
photoUrl = result.user.photoUrl
});
now in the component that you are passing the data to, you can get the keys using the getKey method of react-navigation
TargetComponent.js
render() {
email = this.props.navigation.getParam("email", defaultValue);
password= this.props.navigation.getParam("password", defaultValue);
}

How to use react context with nested mobx stores?

I have two stores: formStore and profileStore
FormStore
export class ProfileFormStore {
#observable editing = false;
profileStore = new ProfileStore(this.roleId);
originalValue?: ApiModel | null;
#action.bound
startEdit() {
// this.originalValue = this.profileStore.toJson();
/* if uncomment above, next error thrown
RangeError: Maximum call stack size exceeded
at initializeInstance (mobx.module.js:391)
at ProfileStore.get (mobx.module.js:381)
at ProfileStore.get
*/
this.editing = true;
}
}
ProfileStore
export class ProfileStore {
#observable userProfile: ApiModel = {
userProfile: {
newsAndUpdates: false,
email: "",
phone: "",
lastName: "",
firstName: "",
},
};
#observable email = "";
#action.bound
fetch() {
// this.fromJson(this.actions.fetch());
console.log("start");
this.email = "qwe";
console.log("end");
}
#computed
toJson(): ApiModel {
return {
userProfile: {
firstName: this.userProfile.userProfile.firstName,
lastName: this.userProfile.userProfile.lastName,
phone: this.userProfile.userProfile.phone,
email: this.userProfile.userProfile.email,
newsAndUpdates: this.userProfile.userProfile.newsAndUpdates,
},
};
}
}
And I want to use contexts
const formStore = new ProfileFormStore();
export const profileFormContext = React.createContext({
formStore,
profileStore: formStore.profileStore,
});
export const useProfileContext = () => React.useContext(profileFormContext);
And there are two components: form and formControl
const controls = {
admin: (<><ProfileName /><Email /></>),
user: (<><ProfileName /></>)
};
export const Form = () => {
const { formStore, profileStore } = useProfileContext();
// this.fromJson(this.actions.fetch()); // if uncomment throws 'Missing option for computed get'
return <form>(controls.admin)</form>
}
export const ProfileName = () => {
const { formStore, profileStore } = useProfileContext();
formStore.startEdit(); // check form store, when assigning from profileStore get overflow error
return formStore.editing ? <input value='test' /> : <label>Test</label>
}
So there are two kinds of errors:
When accessing observables from ProfileStore that is part of FormStore
When updating observables in ProfileStore that is part of FormStore
the FormStore working well
both stores injecting via React.useContext have followed these example https://mobx-react.js.org/recipes-context , however their stores are not nested. I made them nested, beacuse I wanted to get access to profileStore from formStore
What do these errors mean? How to fix them?
Actually it is not the answer :) But the solution I have used
export class ProfileStore {
#observable editing;
#observablt userProfile: UserProfile;
...
}
That's all - instead of using two stores, now there is one store, I happy that solution is working. I assume that error was that I forgot to write get at toJson. If in future I encounter same error and understand why it happened. I will try not to forget to update this answer.

React Redux and side effects explanation

I'm wrapping my forms to provide automatic validation (I don't want to use redux-form).
I want to pass an onSubmit handler which must be fired after every input in form is validated: but how do I wait for form.valid property to turn into true && after wrapping submit was fired? I'm missing some logic here!
//in my Form.js hoc wrapping the forms
#autobind
submit(event) {
event.preventDefault();
this.props.dispatch(syncValidateForm({ formName: this.props.formName, form: this.props.form }));
// ==> what should I do here? Here I know submit button was pressed but state is not updated yet with last dispatch result reduced!
//if(this.props.form.valid)
// this.props.submit();
}
render() {
return (
<form name={this.props.formName} onSubmit={this.submit}>
{ this.props.children }
</form>
);
//action.js validating given input
export const syncValidateInput = ({ formName, input, name, value }) => {
let errors = {<computed object with errors>};
return { type: INPUT_ERRORS, formName, input, name, value: errors };
};
//action.js validating every input in the form
export const syncValidateForm = ({ formName, form }) => {
return dispatch => {
for(let name in form.inputs) {
let input = form.inputs[name];
dispatch(syncValidateInput({ formName, input, name: input.name, value: input.value }));
}
};
};
//in my reducer I have
case INPUT_ERRORS:
let errors = value;
let valid = true;
let errorText = '';
_.each(errors, (value, key) => {
if(value) {
valid = false;
errorText = `${errorText}${key}\n`;
}
});
form.inputs[name].errors = errors;
form.inputs[name].valid = valid;
form.inputs[name].errorText = errorText;
_.each(form.inputs, (input) => form.valid = !!(form.valid && input.valid));
return state;
Help!
Depending on your build config you could use Async/Await for your submit function. Something like
async submit(event) {
event.preventDefault();
const actionResponse = await this.props.dispatch(syncValidateForm({ formName: this.props.formName, form: this.props.form }));
if (actionResponse && this.props.form.valid) { //for example
// finish submission
}
}
And I think you will need to update your syncValidateForm slightly but this should put you on the right path.

Resources