Wait for redux-toolkit variable to update - react-native

What I want to do:
I want to render 4 components in a parent component. How they look like and what they do depends on the settings. The settings can be retrieved from the backend with an API.
How I'm trying to do it:
There is a redux store that handles the API request to fetch the settings. Once the data is fetched (once) then the fetched data can be used for all 4 components. The fetching happens when a component is rendered.
The problem I encounter:
All 4 components call the fetch API (it should only be called once) because the redux variable (fetchStatus) only updates at the end of a render and the components don't know (until next render) that the fetchStatus is already set to "loading". The components still read that the fetchstatus is "idle" and thus call the fetch API as well.
More info context to the problem:
I have a (simplified) slice:
interface settingsState {
settings: Settings;
fetchStatus: CallStatus;
fetchError: null | any;
}
const _initialState: settingsState = {
userSettings: null,
fetchStatus: CallStatus.IDLE,
fetchError: null,
};
export const activeSettingsSlice = createSlice({
name: ‘settings',
initialState: _initialState,
reducers: {},
extraReducers(builder) {
builder.addCase(fetchSettings.pending, (state, _) => {
state.fetchStatus = CallStatus.LOADING;
console.log('loading settings');
console.log('fetchstatus after setting in redux:',state.fetchStatus);
});
builder.addCase(fetchActiveSettings.fulfilled, (state, action) => {
state.fetchStatus = CallStatus.SUCCESS;
state.settings = action.payload.settings;
console.log('fetch settings success');
});
builder.addCase(fetchActiveSettings.rejected, (state, action) => {
state.fetchStatus = CallStatus.FAIL;
state.fetchError = action.payload;
});
},
});
export const fetchSettings = createAsyncThunk('settings/fetch', async (_, thunkApi) => {
new SettingsApi(…).getSettings(…)
);
});
And I have four components like this rendered in the same parent component at the same time:
export default function DemoComponent() {
const fetchStatus = useSelector((state) => state.settings.fetchStatus);
const fetchError = useSelector((state) => state.settings.fetchError);
const dispatch = useDispatch();
useEffect(() => {
if (fetchStatus === CallStatus.IDLE) {
console.log('fetchstatus when should be idle:', fetchStatus);
dispatch(fetchSettings());
}
}, [fetchStatus, dispatch]);
useEffect(() => {
console.log('status fetchstatus:', fetchStatus);
}, [fetchStatus]);
return <></>;
}
However, in my backend I see that the API is called three times.
The API should only be called once (because if it's fetched once, all the other components can read the fetched data (settings)).
If I check the console ouput of react-native I see this:
LOG fetchstatus when should be idle: 0
LOG loading settings
LOG fetchstatus after setting in redux: 1
LOG status fetchstatus: 0 // we just set this variable to 1, and it’s still 0 (so it hasn’t rendered yet right?)
LOG fetchstatus when should be idle: 0
LOG loading settings
LOG fetchstatus after setting in redux: 1
LOG status fetchstatus: 0
LOG fetchstatus when should be idle: 0
LOG loading settings
LOG fetchstatus after setting in redux: 1
LOG status fetchstatus: 0
LOG fetchstatus when should be idle: 0
LOG loading settings
LOG fetchstatus after setting in redux: 1
LOG status fetchstatus: 0
LOG status fetchstatus: 1
LOG status fetchstatus: 1
LOG status fetchstatus: 1
LOG status fetchstatus: 1
LOG fetch settings success
LOG status fetchstatus: 2
LOG status fetchstatus: 2
LOG status fetchstatus: 2
LOG status fetchstatus: 2
LOG fetch settings success
LOG fetch settings success
LOG fetch settings success
To me it seems clear that fetchStatus in DemoComponent has yet to render.
So these are the possible solutions I see:
Only call the dispatch function once in the parent component.
This isn't great as I only want the API to be called when the components are needed.
Wait until render by setting a delay (setTimeout)
You don't know the render duration so this isn't a great solution
I'm hoping there might be a better solution or that there is a mistake in my implementation.

Thanks to the comment from #Linda Paiste,
AsyncThunk has a third optional variable that allows you to add a condition to the asyncThunk where you can also read the Rootstate variables from the slice.
See also https://github.com/reduxjs/redux-toolkit/pull/513
The new asyncThunk:
export const fetchSettings = createAsyncThunk('settings/fetch', async (_, thunkApi) => {
new SettingsApi(…).getSettings(…)
},{condition(args,{getState})=>{return (getState as Rootstate).settings.fetchStatus !== CallStatus.LOADING;}});

Related

Lazy loading & loading states with vue-router, vite & vuejs 2.x

I'm migrating an old project from vue-cli to vite. I followed the migration guide and everything worked great, but there's something it's not working, or at least, not as intended, when I was using vue-cli I tried to implement the loading states as shown in their documentation but then I saw the following pull request explaining how to achieve the wanted behavior (with the downside of losing navigation guards on those routes).
Now, after migrating I noticed that neither the loading/error components are rendered at all, even setting a timeout really small, however, I see in the networking tab that my components are being loaded, but never rendered.
Do you have any suggestions of why might this occur?.
// This is the lazyLoadView that was working before migrating to vite.
function lazyLoadView(AsyncView) {
const AsyncHandler = () => ({
component: AsyncView,
// A component to use while the component is loading.
loading: import("./components/loaders/loader.vue").default,
// A fallback component in case the timeout is exceeded
// when loading the component.
error: import("./components/loaders/error.vue").default,
// Delay before showing the loading component.
// Default: 200 (milliseconds).
delay: 1,
// Time before giving up trying to load the component.
// Default: Infinity (milliseconds).
timeout: 2,
});
return Promise.resolve({
functional: true,
render(h, { data, children }) {
// Transparently pass any props or children
// to the view component.
return h(AsyncHandler, data, children);
},
});
}
And the routes I have:
const routes = [
{
path: "/foo/",
component: () => lazyLoadView(import("./components/bar.vue")),
}
]
Let me know if you find why might this be happening.
So I figured out:
Looks like the loading & error components were also lazy loaded, they were skipped. So continued trying to obtain the main one until shown (that's why didn't render the loading besides of only showing a message).
So in order to fix it I had to import them at the top and include them in the lazyLoadView like this:
//These two imports at the top
import loaderComp from "./components/loaders/loader.vue";
import errorComp from "./components/loaders/error.vue";
function lazyLoadView(AsyncView) {
const AsyncHandler = () => ({
component: AsyncView,
// A component to use while the component is loading.
// must NOT be lazy-loaded
loading: loaderComp,
// A fallback component in case the timeout is exceeded
// when loading the component.
// must NOT be lazy-loaded
error: errorComp,
// Delay before showing the loading component.
// Default: 200 (milliseconds).
delay: 1,
// Time before giving up trying to load the component.
// Default: Infinity (milliseconds).
timeout: 2,
});
return Promise.resolve({
functional: true,
render(h, { data, children }) {
// Transparently pass any props or children
// to the view component.
return h(AsyncHandler, data, children);
},
});
}

Next.JS Abort fetching component for route: "/login"

I was developing a useUser Hook for per-page authentication. I have implemented the useUser hook normally and Redirecting works fine accordingly.
But I am getting the above error.
Abort fetching component for route: "/login"
How can I fix useUserHook to solve it??
//useUser.tsx
const useUser = ({ redirectTo, redirectIfFound }: IParams) => {
const { data, error } = useRequest("authed", isAuthed);
const user = data?.data;
const hasUser = user;
useEffect(() => {
if (!redirectTo) return;
if (
// If redirectTo is set, redirect if the user was not found.
(redirectTo && !redirectIfFound && !hasUser) ||
// If redirectIfFound is also set, redirect if the user was found
(redirectIfFound && hasUser)
) {
Router.push(redirectTo);
}
}, [redirectTo, redirectIfFound, hasUser]);
return error ? null : user;
};
//index.tsx
const Home: NextPage = () => {
const user = useUser({ redirectTo: "/login" });
if (user === undefined || user === false) {
return <div>Loading...</div>;
}
return (
<div>
<Head>
<meta name="description" content="Generated by create next app" />
<link rel="icon" href="/favicon.ico" />
</Head>
<div>Home</div>
</div>
);
};
UseRequest Hook returns true and false as return values.
tl;dr
Ensure that you only call router.push() once throughout all potential re-executions of useEffect with the help of state:
const [calledPush, setCalledPush] = useState(false); // <- add this state
// rest of your code [...]
useEffect(() => {
if (!redirectTo) return;
if (
(redirectTo && !redirectIfFound && !hasUser) ||
(redirectIfFound && hasUser)
) {
// check if we have previously called router.push() before redirecting
if (calledPush) {
return; // no need to call router.push() again
}
Router.push(redirectTo);
setCalledPush(true); // <-- toggle 'true' after first redirect
}
}, [redirectTo, redirectIfFound, hasUser]);
return error ? null : user;
};
Background
useEffect potentially gets called multiple times if you have more than one dependency (Also happens with React Strict Mode enabled, but in this case there seems to be no error), and (re-)calling router.push() multiple times within the same Next.js page in different places/throughout different re-renders seems to cause this error in some cases, as the redundant router.push() call(s) will have to be aborted, because the current page-component unmounts due to the successful, previously called router.push().
If we keep track of whether we have already called router.push via the calledPush state as in the code snippet above, we omit all redundant router.push() calls in potential useEffect re-executions, because for all subsequent useEffect executions the state value calledPush will already be updated to true as useEffect gets triggered after re-renders, hence after setCalledPush(true) takes effect.
In my case I have use rotuer.push("/") two times in a single file. That caused the error. Try using one. I think problem will be solved.
This error occurs because useEffect tries to update a component that has already been unmounted and this can introduce memory leaks in which your app uses more memory than it needs to. To prevent this, use the following approach:
useEffect(() => {
//first manually mount the effect
let mounted = true;
//check if component is currently mounted
if(mounted && ...code){
router.push('/index')
}
//cleanup side effects before unmounting
return()=>{mounted=false}
}, [router]);
};
In my current NextJS project, when I make reactStrictMode: false,, the value to false, then it seems like the re-rendering will be gone, and component will be only rendered once.
I don't like react strict mode very much ..

casl - can() returns null after reloading (F5) page

I'm trying to setting up CASL permissions system into my VueJS 2 project.
All works fine, until I intentionally refresh (Like F5). All can() returns false even if my user have the abilities.
router / index.js :
router.beforeEach((to, _, next) => {
const isLoggedIn = isUserLoggedIn()
if (!canNavigate(to)) {
// Redirect to login if not logged in
if (!isLoggedIn) return next({ name: 'auth-login' })
// If logged in => not authorized
return next({ name: 'misc-not-authorized' })
}
// Redirect if logged in
if (to.meta.redirectIfLoggedIn && isLoggedIn) {
const userData = getUserData()
next(getDashboardRoute(userData ? userData.role : null))
}
return next()
})
canNavigate(to) function :
export const canNavigate = to => ability.can(to.meta.action || 'read', to.meta.resource)
user abilities (from localStorage) :
route configuration :
export default [
{
path: '/route-test/',
name: 'route-test',
component: () => import('#/views/TestRoute.vue'),
meta: {
resource: 'ADB',
action: 'read',
},
},
]
So, canNavigate returns false, and I'm getting a Maximum call stack size exceeded error but, this is normal due to the "infinite" loop with in my beforeEach router function...
Why do my canNavigate returns false... after refresh?
Thanks to everyone give time to help me :)
I'm facing the same issue with a Vue 2 project - everytime i refresh the page the $ability instance gets reseted to its default value (before the .update() is called). Idk what's the root cause of the problem, but what i made solves the problem for now: I update the $ability instance each time the entry point component (in my case Home component) is mounted(). Idk if it is a good solution, but worked for me (if anyone knows a better way to do it please let me know).

Handling 401 responses in a react-native functional component

I'm getting the following warning when an API request returns 401 and I send the user to the login screen:
Warning: Can't perform a React state update on an unmounted component.
What is the best way to handle this warning in a functional component that uses hooks. See the code below:
.
.
export default function MovieDetailsScreen() {
const [movie, setMovie] = useState({});
const movieId = useNavigationParam('movieId');
useEffect(() => {
// This is the method that does the request and returns 401 (It
// uses the fetch library)
Client.movies.show(movieId)
.then(result => {
setMovie(result)
})
}, [])
.
.
.
In general, warnings don't crash your application. But you should care about them. For instance, the previous warning(s) can lead to performance issues when not properly unmounting your stateful components
the request (e.g. Promise) isn't resolved yet, but you unmount the component. Then the request resolves, setMovie() is called to set the new state, but it hits an unmounted component.
You can try catch
Client.movies.show(movieId)
.then(result => {
setMovie(result)
})
.catch((err) => {
Client.movies.stop()
})

Waiting for async before register component in react-native

I need to wait for async storage and then init app, because I store auth token here and want to show correct scene for user if he was authorised:
(async () => {
const viewer = JSON.parse(await AsyncStorage.getItem('viewer'));
// ...
const RootContainer = () => (
// ...
);
AppRegistry.registerComponent('yawaloo', () => RootContainer);
})();
I have moved to react-native 0.40.0 from 0.34.1 and now have an error "Module AppRegistry is not a registered callable".
In previous version everything were ok. How can I wait for some actions and then start render RootContainer?
One idea is to use splash screen. More specifically use a state in your RootContainer to determine whether to show a splash screen or your main UI. Set the state to false (show splash) initially then after you read the token from async storage, then set the state to true.
Part of why apps have splash screens is to deal with situation like this. HTH