I'm experiencing some odd behaviour with a custom hook that I've written. I use it as a staging ground to update information locally, before the user commits that information and sends it off to the database. This hook is called in 2 different places: when the user is creating an item (a nugget), and when a user is editing an item.
Naturally, we want each scenario to have its own state, and to not have data leak between the two. However, this is exactly what happens. In the EditScreen, I call the hook, passing it the data that already exists (nuggetInput). In the NewScreen, I also call the hook, passing in nothing. Strangely, when I go to the EditScreen, the hook implementation in NewScreen gets updated. Of course, I'd expect these 2 hooks to be isolated from one another.
To test if this was in fact the case, I separated this useStaging hook out into 2 identical hooks: useNewStaging and useEditStaging. Sure enough, this works and there are no leaks.
Here is the useStaging hook in question (with parts removed for brevity):
useStaging.js
let initialState = {
title: null,
mediaItems: [],
}
const reducer = (state, { type, ...action }) => {
switch (type) {
case 'TITLE_UPDATE':
return produce(state, (draft) => {
const { data } = action
draft.title = data
})
default:
throw new Error('Called reducer without supported type.')
}
}
export default (nuggetInput) => {
if (nuggetInput) {
initialState = merge(initialState, nuggetInput)
}
const [state, dispatch] = useReducer(reducer, initialState)
const updateTitle = (data) => dispatch({ type: 'TITLE_UPDATE', data })
return {
nuggetStaging: state,
updateTitle,
}
}
NewScreen.js
const NewScreen = () => {
const { nuggetStaging, updateTitle } = useStaging()
}
EditScreen.js
const EditScreen = ({ inputNugget }) => {
const { nuggetStaging, updateTitle } = useStaging(inputNugget)
}
Any idea why this would be happening?
Related
I need to display a list of objects in my Explorer component.
In my app I use useReducer hook what wrapped by Context.
It works well when the data flow is in "input-mode" (when I update data in state). But it does not rerender the application after data was changed.
So, steps that I need to pass and get a positive result.
Press btn with file icon (or folder icon). This btn call hook that take a look into state and make a decision: where this file or folder should be placed in my simple fs.
Write file/folder name (this function doesn't exist yet).
Apply name by press enter or click mouse out of input (the same as 2 step).
Currently, I try to create file/folder with hardcoded name for testing 1-step. And I expect that dispatch function pass data to the state and it would be updated and rerendered. All process runs well except of rerender.
I explain the flow of 1-st step.
After I click btn, I call the func from hook for forming my instance.
Then, the new instance saving into local useState.
After local useState successfully was updated, I call dispatch in useEffect hook.
In reducer I modify my state and return it.
After this steps I expect that my app will automatically rerendered, but it isn't.
Code snippets, step by step.
First step.
const handleFileClick = () => {
formAnInstance('file');
console.log('file btn click')
};
Second step.
// in useInstancesInteraction hook
const { state, dispatch } = useStateContext();
const [instance, setInstance] = useState<IInstance>();
const formAnInstance = (mode: Omit<Mode, 'root'>) => {
if (
typeof state?.currentFolder === 'undefined' ||
state?.currentFolder === null
) {
const target =
mode === 'folder'
? (createInstance('folder', 'folder') as IInstance)
: (createInstance('file', 'file') as IInstance);
target['..'] = '/';
setInstance(target);
}
};
Third step.
// in useInstancesInteraction hook
useEffect(() => {
const updateData = () => {
if (dispatch && instance) {
dispatch(createRootInstance(instance));
}
};
updateData();
}, [instance]);
Fourth step.
export const initialState = {
root: createInstance('/', 'root') as IInstance,
currentFolder: null,
};
const reducer = (state = initialState, action: IAction) => {
const { type, payload } = action;
switch (type) {
case ACTION_TYPES.CREATE_ROOT_INSTANCE:
const myKey = payload['.'];
Object.assign(state.root, { [myKey]: payload });
console.log('Reducer', state?.root);
return state;
case ACTION_TYPES.CREATE_FILE:
break;
case ACTION_TYPES.UPLOAD_FILE:
break;
case ACTION_TYPES.RENAME_FILE:
break;
case ACTION_TYPES.DELETE_FILE:
break;
case ACTION_TYPES.CREATE_FOLDER:
break;
case ACTION_TYPES.RENAME_FOLDER:
break;
case ACTION_TYPES.DELETE_FOLDER:
break;
default:
return state;
}
};
Here how my context file look like:
import React, { useContext, useReducer } from 'react';
import { IContext } from './index.types';
import reducer, { initialState } from './reducer';
const StateContext = React.createContext<IContext>({
state: undefined,
dispatch: null,
});
const StateProvider = ({
children,
}: {
children: JSX.Element | JSX.Element[];
}) => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<StateContext.Provider value={{ state, dispatch }}>
{children}
</StateContext.Provider>
);
};
export default StateProvider;
export const useStateContext = () => useContext(StateContext);
I'm having this weird issue where my RTK Query requests are happening in a strange order.
We've got the RTK sports slice, and in the same file I've defined the useLoadSports hook
const sportsSlice = createSlice({
name: 'sports', initialState: {},
reducers: {
setSports: (state, action) => action.payload,
},
});
export const sports = sportsSlice.reducer;
export const { setSports } = sportsSlice.actions;
export const useLoadSports = () => {
const dispatch = useDispatch();
const { data: sports, ...result } = useGetSportsQuery();
useEffect(() => { console.log('useLoadSports'); }, []);
useEffect(() => {
if (sports) {
console.log('SETTING SPORTS');
dispatch(setSports(sports));
}
}, [sports]);
return result;
};
The Application component uses this hook as it loads some data needed throughout the app.
const useInitialLoad = () => {
useEffect(() => {
console.log('useInitialLoad');
}, []);
const { isLoading: sportsLoading } = useLoadSports(); // below
const ready = !sportsLoading;
return { ready };
};
const Application: React.FC<Props> = () => {
const { ready } = useInitialLoad();
if (!ready) return <h1>Loading app data</h1>;
return (
<S.Wrapper>
<AppRouter />
</S.Wrapper>
);
};
The AppRouter actually iterates over a config object to create Routes. I'm assuming that's not our issue here.
Anyway, then the PlayerPropsPage component calls useGetPropsDashboardQuery.
const PlayerPropsPage = () => {
const { data, isLoading, isError, error } = useGetPropsDashboardQuery();
useEffect(() => {
console.log('LOADING PlayerPropsPage');
}, [])
return /* markup */
}
The query's queryFn uses the sports that were saved into the store by useLoadSports
export const { useGetPropsDashboardQuery, ...extendedApi } = adminApi.injectEndpoints({
endpoints: build => ({
getPropsDashboard: build.query<PropAdminUIDashBoard, void>({
queryFn: async (_args, { getState }, _extraOptions, baseQuery) => {
console.log('PROPS ENDPOINT');
const result = await baseQuery({ url });
const dashboard = result.data as PropAdminDashBoard;
const { sports } = getState() as RootState;
if (!Object.entries(sports).length) {
throw new Error('No sports found');
}
// use the sports, etc.
},
}),
}),
});
I'd think it would use the setSports action before even rendering the router (and hence calling the props endpoint or loading the page, and I'd really think it would render the PlayerPropsPage before calling the props query, but here's the log:
useInitialLoad
useLoadSports
LOADING PlayerPropsPage
PROPS ENDPOINT
SETTING SPORTS
Another crazy thing is if I move the getState() call in the endpoint above the call to baseQuery, the sports haven't been stored yet, and the error is thrown.
Why is this happening this way?
A bunch of random observations:
you should really not dispatch that setSports action in a useEffect here. If you really want to have a slice with the result of your useGetSportsQuery, then add an extraReducers for api.endpoints.getSports.fulfilled. See this example:
// from the example
extraReducers: (builder) => {
builder.addMatcher(
api.endpoints.login.matchFulfilled,
(state, { payload }) => {
state.token = payload.token
state.user = payload.user
}
)
},
I don't see why you even copy that data into the state just to use a complicated queryFn instead of just passing it down as props, using a query and passing it in as useGetPropsDashboardQuery(sports). That way that getPropsDashboard will update if the sports argument changes - which will never happen if you take the extra logic with the getState() and all the other magic.
you could even simplify this further:
const { data: sports } = useGetSportsQuery()
const result = useGetPropsDashboardQuery( sports ? sports : skipToken )
No need for a slice, no need to have that logic spread over multiple components, no need for a queryFn. queryFn is an escape hatch and it really doesn't seem like you need it.
The current behaviour is normal even if it's not what you expect.
you have a main hook who do a query
the first query start
when the query is finish it does "rerender" and does this
dispatch the result of the sports
the second query start
So there is no guarantee that you have sports in the store when you start the second query. As all is done with hooks (you can technically do it with hooks but that's another topic)
How to wait a thunk result to trigger another one ?
You have multiple ways to do it. It depends also if you need to wait thoses two queries or not.
Listener middleware
If you want to run some logic when a thunk is finish, having a listener can help you.
listenerMiddleware.startListening({
matcher: sportsApi.endpoints.getSports.fulfilled,
effect: async (action, listenerApi) => {
listenerApi.dispatch(dashboardApi.endpoints.getPropsDashboard.initiate())
}
},
})
In addition, instead of setting sports in the store with a dispatch inside the useEffect. You can plug your query into the extraReducers. here is an example:
createSlice({
name: 'sports',
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addMatcher(
sportsApi.endpoints.getSports.fulfilled,
(state, action) => {
state.sports = action.payload.sports
}
)
},
})
Injecting thunk arguments with a selector
If you use directly pass sports as a variable to the query when they change they'll re-trigger the query. here is an example:
const PlayerPropsPage = () => {
const { data: sports, ...result } = useGetSportsQuery();
const { data, isLoading, isError, error } = useGetPropsDashboardQuery(sports);
}
Doing the two query inside a single queryFn
If inside a single queryFn you can chain theses query by awaiting them
queryFn: async (_args, { getState }, _extraOptions, baseQuery) => {
const resultFirstQuery = await baseQuery({ url: firstQueryUrl });
const resultSecondQuery = await baseQuery({ url: secondQueryUrl });
// Do stuff
},
Note:
When you use getState() inside a thunk, if the store update this will not trigger your thunk "automatically"
I do not know if you need the sport to do the second query or to group the result of the two queries together.
I have this code:
const { user } = useSelector((state) => state.userReducer);
const { other } = useSelector((state) => state.otherReducer);
How could I get in one line those two (or more) values from the store?
const { user, other } = useSelector((state) => ?);
const { user, other } = useSelector((state) => ({
user: state.userReducer,
other: state.otherReducer
}))
If you use lodash:
import { pick, isEqual } from 'lodash-es';
const { userReducer, otherReducer } = useSelector(
(state) => pick(state, ['userReducer', 'otherReducer']),
isEqual
)
The second param (isEqual) is aimed at resolving the potential performance issue mentioned by #NicholasTower (and is equivalent to react-redux's shallowEqual).
Without a shallow object equality function, the component will re-render every time any mutation is committed to that store, regardless of key.
The naive way to do it on one line would be to write a function that grabs the parts of state you care about, and constructs an object with those properties:
const { user, other } = useSelector((state) => {
return { user: state.userReducer, other: state.otherReducer };
});
However, this will immediately cause a performance problem. Every time the selector runs, it creates a brand new object. And even if user and other havn't changed, the outer object has changed, so your component is forced to rerender. In other words, your component will rerender every time any action is dispatched anywhere in the app.
To fix this, useSelector allows you to provide an additional function which defines whether two values are equal or not. You can then implement a function to check if the stuff you care about has changed:
const { user, other } = useSelector((state) => {
return { user: state.userReducer, other: state.otherReducer };
}, (a, b) => {
return a.user === b.user && a.other === b.other
});
react-redux comes with a shallow comparison function which may be useful for this purpose if you prefer:
import { useSelector, shallowEqual } from 'react-redux';
// ...
const { user, other } = useSelector((state) => {
return { user: state.userReducer, other: state.otherReducer };
}, shallowEqual);
Would suggest using array instead of objects to avoid repeating keys
const [user, other] = useSelector(state => [state.userReducer, state.otherReducer])
Meh, it was:
const { userReducer: { user }, otherReducer: { other } } = useSelector((state) => state);
My context looks like this:
class AuthStoreClass {
authUser = null
constructor() {
makeAutoObservable(this)
}
login = async (params) => {
const { data: { data: authUser } } = await loginUser(params)
this.authUser = authUser
}
}
const AuthStoreContext = React.createContext(null);
export const authStoreObject = new AuthStoreClass()
export const AuthStoreProvider = ({ children }: any) => {
return <AuthStoreContext.Provider value={authStoreObject}>{children}</AuthStoreContext.Provider>;
};
export const useAuthStore = () => {
return React.useContext(AuthStoreContext);
};
And I am using the context somewhere else in a component:
const LoginPage = observer(() => {
const authStore = useAuthStore()
...
authStore.login(...)
The last line reports the following warning:
[MobX] Since strict-mode is enabled, changing (observed) observable values without using an action is not allowed. Tried to modify: AuthStoreClass#1.authUser
Everything works as expected. How can I fix this issue?
Your login function is async and you need to use runInAction inside, or handle result in a separate action, or use some other way of handling async actions:
import { runInAction, makeAutoObservable } from "mobx"
class AuthStoreClass {
authUser = null
constructor() {
makeAutoObservable(this)
}
login = async (params) => {
const { data: { data: authUser } } = await loginUser(params)
// Wrap all changes with runInAction
runInAction(() => {
this.authUser = authUser
})
// or do it in separate function
this.setUser(authUser)
}
// This method will be wrapped into `action` automatically by `makeAutoObservable`
setUser = (user) => {
this.authUser = user
}
}
That is because, citing the docs, every step ("tick") that updates observables in an asynchronous process should be marked as action. And the code before the first await is in a different "tick" than the code after await.
More about async actions (you can even use generators!): https://mobx.js.org/actions.html#asynchronous-actions
In MobX version 6 actions are enforced by default but you can disable warnings with configure method:
import { configure } from "mobx"
configure({
enforceActions: "never",
})
But be careful doing it though, the goal of enforceActions is that you don't forget to wrap event handlers and all mutations in an action. Not doing it might cause extra re-runs of your observers. For example, if you changing two values inside some handler without action then your component might re-render twice instead of once. makeAutoObservable wraps all methods automatically but you still need to handle async methods and Promises manually.
You can also change the function to use the yield syntax, negating the need for runInAction.
*login() {
const { data: { data: authUser } } = yield loginUser(params)
this.authUser = authUser
}
I have value X coming from the server. I would like to expose an interface similar to
interface Xclient {
getX(): Promise<X>
}
that I will later use from my react function component.
I say similar because behind the scenes I want it to:
first return value from the storage (its react-native)
simultaneously dispatch network call for newer version of X and re-render the component once I have the response
so instead of Promise I probably need Observable. But then how to use it with react in general and react hooks in particular?
I'm coming from the backend background so there may be some more canonical approach that I dont know about. I really dont want to use redux if possible!
If i understand correctly you have two data source (local storage and your api) and you want to get value from local storage and then get actual value from api. So, you should make next:
import { useState } from "react";
const localProvider: Xclient = new Something();
const apiProvider: Xclient = new SomethingElse();
export function SimpleView() {
const [state, setState] = useState("default value");
localProvider()
.then((response) => {
setState(response);
apiProvider()
.then((actualResponse) => {
setState(actualResponse);
})
.catch(/* */);
})
.catch(/* */);
}
But I see no reason for call it synchronously and you can want to run it parallel:
import { useState } from "react";
const localProvider: Xclient = new Something();
const apiProvider: Xclient = new SomethingElse();
export function SimpleView() {
const [state, setState] = useState("default value");
localProvider()
.then((response) => {
setState(response);
})
.catch(/* */);
apiProvider()
.then((actualResponse) => {
setState(actualResponse);
})
.catch(/* */);
}
If you want to encapsulate this logic you can make a function like this:
import { useState } from "react";
const localProvider: Xclient = new Something();
const apiProvider: Xclient = new SomethingElse();
function getValue(localProvider, apiProvider, consumer) {
localProvider()
.then((response) => {
consumer(response);
})
.catch(/* */);
apiProvider()
.then((actualResponse) => {
consumer(actualResponse);
})
.catch(/* */);
}
export function SimpleView() {
const [state, setState] = useState("default value");
getValue(localProvider, apiProvider, setState);
}
UPD:
As #PatrickRoberts correctly noticed my examples contain the race condition, this code solves it:
import { useState } from "react";
const localProvider: Xclient = new Something();
const apiProvider: Xclient = new SomethingElse();
function getValue(localProvider, apiProvider, consumer) {
let wasApiResolved = false;
localProvider()
.then((response) => {
if (!wasApiResolved) {
consumer(response);
}
})
.catch(/* */);
apiProvider()
.then((actualResponse) => {
wasApiResolved = true;
consumer(actualResponse);
})
.catch(/* */);
}
export function SimpleView() {
const [state, setState] = useState("default value");
getValue(localProvider, apiProvider, setState);
}
I would personally write a custom hook for this to encapsulate React's useReducer().
This approach is inspired by the signature of the function Object.assign() because the sources rest parameter prioritizes later sources over earlier sources as each of the promises resolve:
import { useEffect, useReducer, useState } from 'react';
function useSources<T> (initialValue: T, ...sources: (() => Promise<T>)[]) {
const [fetches] = useState(sources);
const [state, dispatch] = useReducer(
(oldState: [T, number], newState: [T, number]) =>
oldState[1] < newState[1] ? newState : oldState,
[initialValue, -1]
);
useEffect(() => {
let mounted = true;
fetches.forEach(
(fetch, index) => fetch().then(value => {
if (mounted) dispatch([value, index]);
});
);
return () => { mounted = false; };
}, [fetches, dispatch]);
return state;
}
This automatically updates state to reference the result of the latest available source, as well as the index of the source which provided the value, each time a promise resolves.
The reason we include const [fetches] = useState(sources); is so the reference to the array fetches remains constant across re-renders of the component that makes the call to the useSources() hook.
That line could be changed to const fetches = useMemo(() => sources); if you don't mind the component potentially making more than one request to each source during its lifetime, because useMemo() doesn't semantically guarantee memoization. This is explained in the documentation:
You may rely on useMemo as a performance optimization, not as a semantic guarantee. In the future, React may choose to "forget" some previously memoized values and recalculate them on next render, e.g. to free memory for offscreen components.
Here's an example usage of the useSources() hook:
const storageClient: Xclient = new StorageXclient();
const networkClient: Xclient = new NetworkXclient();
export default () => {
const [x, xIndex] = useSources(
'default value',
() => storageClient.getX(),
() => networkClient.getX()
);
// xIndex indicates the index of the source from which x is currently loaded
// or -1 if x references the default value
};