Update global state after RTK Query loads data - redux-toolkit

I've noticed a problem with splitting responsibilities in React components based on the fetched data using RTK Query.
Basically, I have two components like HomePage and NavigationComponent.
On HomePage I'd like to fetch the information about the user so that I can modify NavigationComponent accordingly.
What I do inside HomePage:
import { setNavigationMode } from "features/nav/navSlice";
export default function HomePage() {
const {data: user} = useGetUserDataQuery();
const dispatch = useAppDispatch();
const navMode = user ? "all-options" : "none";
dispatch(setNavigationMode(navMode)); // here I change the default Navigation mode
return <MainLayout>
<Navigation/>
<Content/>
<Footer/>
</MainLayout>;
}
The HomePage is a special Page when the NavigationComponent shouldn't display any options for the not logged in user.
Other pages presents additional Logo and Title on Nav.
React communicates:
Warning: Cannot update a component (NavComponent) while rendering a different component (HomePage). To locate the bad setState() call inside HomePage, follow the stack trace as described in https://reactjs.org/link/setstate-in-render
Not sure what is the right way to follow.
Whether the state should be changed in GetUser query after it is loaded - that doesn't seem to be legit.

problem is dispatch calls every render. Instead you can create a navigationSlice (if you don't have already) and use extraReducers for matching your authorization action like:
extraReducers: (builder) => {
builder.addMatcher(
usersApi.endpoints.login.matchFulfilled,
(state, { payload }) => {
if (payload.user) {
state.navigationMode = "all-options"
}
}
);
}
This way, state.navigationMode will only change when authorization changes

The solution was too obvious. The dispatch should be run in useEffect.
import { setNavigationMode } from "features/nav/navSlice";
export default function HomePage() {
const {data: user} = useGetUserDataQuery();
const dispatch = useAppDispatch();
const navMode = user ? "all-options" : "none";
// changed lines
useEffect( () => {
dispatch(setNavMode(navMode));
}, [navMode, dispatch]);
// /changed lines
return <MainLayout>
<Navigation/>
<Content/>
<Footer/>
</MainLayout>;
}
Thank you #papa-xvii for the hint with changing the navMode after user login. That solves the second problem I had.
However I cannot accept the answer as it does not solve the problem I described above.

Related

How to get data from an API only once (on app creation, outside component or view) in Vue3 SPA, with Pinia store

Is it possible and is it a good practice to avoid fetching data from an API every time the router view is loaded or the component is Mounted?
The thing is that some data rarely changes (like a dropdown options list, imagine allowed animal picks for my app) and it's logical not to send a request every time to a server, instead on app creation would be sufficient.
Tried in App.vue, is that a common thing?
IN APP.vue
import { computed, onMounted, onUpdated, ref } from 'vue';
onMounted(()=>{
axios.get('/data')....
.then((res)=>{
store.property = res.data
...
})
})
I think having it on mount in the App.vue component is acceptable since the App component would not be remounted.
The ideal setup, however, depends on some other parameters like size of application and size of team that's maintaining it. In a large applications you might want to organize things in amore structured and consistent way so you and other folks working on the code know where to find things.
You could consider moving the API call into the pinia action.
store.loadMyData()
// instead of
axios.get('/data')
.then((res)=>{
store.property = res.data;
})
That way you have fewer lines of code in the component. Having "lean" components and moving "business logic" out of components usually makes for better organization which makes it easier to maintain.
Within the action, you can track the state of the API
const STATES = {
INIT: 0,
DONE: 1,
WIP: 2,
ERROR: 3
}
export const useMyApiStore = defineStore('myapi', {
state: () => ({
faves: [],
favesState: STATES.INIT
}),
actions: {
loadMyData() {
this.store.favesState = STATES.WIP;
axios.get('/data')
.then((res) => {
this.store.property = res.data;
this.store.favesState = STATES.DONE;
})
.catch((e) => {
this.store.favesState = STATES.ERROR;
})
},
},
getters: {
isLoaded(){
return this.store.favesState === STATES.DONE;
}
isLoading(){
return this.store.favesState === STATES.WIP;
}
}
})
This is, obviously, more verbose, but allows for the components to be smaller and contain less logic. Then, for example, in your component you can use the getter isLoading to display a loading indicator, or use isLoaded inside another component to determine whether to show it.
Yes, this is a oft used way to load some data into the Vue App.
You could also load data before the Mounting in beforeMount() or created() Lifecycle Hooks (see Vue Lifecycle Diagram) to prevent unnecessary HTML updates.

Detect if user is connected to firebase app

I want to implement an "isActive" feature on my app that is built using firebase firestore. I am using firebase functions along with authentication in my React App.
Is there a way of detecting if the user is "active" or "inActive" on my app by triggering a cloud function when they login or disconnect to the app?
If i can determine this i would store the value in firestore and handle it in the frontend to display a UI.
Thanks
I see two aspects to the question here:
Auth state: You want to track the logged-in duration of the user.
Focus state: You want to track when the user is active on the app.
For #1, you will have to listen to the auth state changes, and you may also want to change your auth state persistence strategy accordingly. From https://firebase.google.com/docs/auth/web/auth-state-persistence:
Enum
Value
Description
firebase.auth.Auth.Persistence.LOCAL
'local'
Indicates that the state will be persisted even when the browser window is closed or the activity is destroyed in React Native. An explicit sign out is needed to clear that state. Note that Firebase Auth web sessions are single host origin and will be persisted for a single domain only.
firebase.auth.Auth.Persistence.SESSION
'session'
Indicates that the state will only persist in the current session or tab, and will be cleared when the tab or window in which the user authenticated is closed. Applies only to web apps.
firebase.auth.Auth.Persistence.NONE
'none'
Indicates that the state will only be stored in memory and will be cleared when the window or activity is refreshed.
import { getAuth, setPersistence, signInWithRedirect, inMemoryPersistence, GoogleAuthProvider } from "firebase/auth";
const auth = getAuth();
setPersistence(auth, inMemoryPersistence)
.then(() => {
const provider = new GoogleAuthProvider();
// In memory persistence will be applied to the signed in Google user
// even though the persistence was set to 'none' and a page redirect
// occurred.
return signInWithRedirect(auth, provider);
})
.catch((error) => {
// Handle Errors here.
const errorCode = error.code;
const errorMessage = error.message;
});
You don't need a cloud function for th
For #2, you might want to check out this blog. https://javascript.plainenglish.io/validate-your-apps-session-on-focus-892f610f7e23.
import { useLocation } from '#reach/router';
import React, { useEffect } from 'react';
import { useDispatch } from 'react-redux';
export function WindowFocusHandler() {
const dispatch = useDispatch()
const location = useLocation()
useEffect(() => {
window.addEventListener("focus", onFocus)
return () => {
window.removeEventListener("focus", onFocus)
}
}, [])
const onFocus = () => dispatch(session.effects.checkSessionOnFocus(location))
return <></>
}
The Focus event on a window might get triggered more often if it is a browser app. Depending on what you want to achieve, both things should be possible in
your front-end code without the need for cloud functions. Although, you might opt to code the logic as a firebase function and invoke it from your react code when onFocus.

NEXTJS fix window is not defined on import [duplicate]

In my Next.js app I can't seem to access window:
Unhandled Rejection (ReferenceError): window is not defined
componentWillMount() {
console.log('window.innerHeight', window.innerHeight);
}
̶A̶n̶o̶t̶h̶e̶r̶ ̶s̶o̶l̶u̶t̶i̶o̶n̶ ̶i̶s̶ ̶b̶y̶ ̶u̶s̶i̶n̶g̶ ̶p̶r̶o̶c̶e̶s̶s̶.̶b̶r̶o̶w̶s̶e̶r ̶ ̶t̶o̶ ̶j̶u̶s̶t̶ ̶e̶x̶e̶c̶u̶t̶e̶ ̶ ̶y̶o̶u̶r̶ ̶c̶o̶m̶m̶a̶n̶d̶ ̶d̶u̶r̶i̶n̶g̶ ̶r̶e̶n̶d̶e̶r̶i̶n̶g̶ ̶o̶n̶ ̶t̶h̶e̶ ̶c̶l̶i̶e̶n̶t̶ ̶s̶i̶d̶e̶ ̶o̶n̶l̶y̶.
But process object has been deprecated in Webpack5 and also NextJS, because it is a NodeJS variable for backend side only.
So we have to use back window object from the browser.
if (typeof window !== "undefined") {
// Client-side-only code
}
Other solution is by using react hook to replace componentDidMount:
useEffect(() => {
// Client-side-only code
})
Move the code from componentWillMount() to componentDidMount():
componentDidMount() {
console.log('window.innerHeight', window.innerHeight);
}
In Next.js, componentDidMount() is executed only on the client where window and other browser specific APIs will be available. From the Next.js wiki:
Next.js is universal, which means it executes code first server-side,
then client-side. The window object is only present client-side, so if
you absolutely need to have access to it in some React component, you
should put that code in componentDidMount. This lifecycle method will
only be executed on the client. You may also want to check if there
isn't some alternative universal library which may suit your needs.
Along the same lines, componentWillMount() will be deprecated in v17 of React, so it effectively will be potentially unsafe to use in the very near future.
If you use React Hooks you can move the code into the Effect Hook:
import * as React from "react";
export const MyComp = () => {
React.useEffect(() => {
// window is accessible here.
console.log("window.innerHeight", window.innerHeight);
}, []);
return (<div></div>)
}
The code inside useEffect is only executed on the client (in the browser), thus it has access to window.
With No SSR
https://nextjs.org/docs/advanced-features/dynamic-import#with-no-ssr
import dynamic from 'next/dynamic'
const DynamicComponentWithNoSSR = dynamic(
() => import('../components/hello3'),
{ ssr: false }
)
function Home() {
return (
<div>
<Header />
<DynamicComponentWithNoSSR />
<p>HOME PAGE is here!</p>
</div>
)
}
export default Home
The error occurs because window is not yet available, while component is still mounting. You can access window object after component is mounted.
You can create a very useful hook for getting dynamic window.innerHeight or window.innerWidth
const useDeviceSize = () => {
const [width, setWidth] = useState(0)
const [height, setHeight] = useState(0)
const handleWindowResize = () => {
setWidth(window.innerWidth);
setHeight(window.innerHeight);
}
useEffect(() => {
// component is mounted and window is available
handleWindowResize();
window.addEventListener('resize', handleWindowResize);
// unsubscribe from the event on component unmount
return () => window.removeEventListener('resize', handleWindowResize);
}, []);
return [width, height]
}
export default useDeviceSize
Use case:
const [width, height] = useDeviceSize();
componentWillMount() lifecycle hook works both on server as well as client side. In your case server would not know about window or document during page serving, the suggestion is to move the code to either
Solution 1:
componentDidMount()
Or, Solution 2
In case it is something that you only want to perform in then you could write something like:
componentWillMount() {
if (typeof window !== 'undefined') {
console.log('window.innerHeight', window.innerHeight);
}
}
In the constructor of your class Component you can add
if (typeof window === 'undefined') {
global.window = {}
}
Example:
import React, { Component } from 'react'
class MyClassName extends Component {
constructor(props){
super(props)
...
if (typeof window === 'undefined') {
global.window = {}
}
}
This will avoid the error (in my case, the error would occur after I would click reload of the page).
global?.window && window.innerHeight
It's important to use the operator ?., otherwise the build command might crash.
Best solution ever
import dynamic from 'next/dynamic';
const Chart = dynamic(()=> import('react-apexcharts'), {
ssr:false,
})
A bit late but you could also consider using Dynamic Imports from next turn off SSR for that component.
You can warp the import for your component inside a dynamic function and then, use the returned value as the actual component.
import dynamic from 'next/dynamic'
const BoardDynamic = dynamic(() => import('../components/Board.tsx'), {
ssr: false,
})
<>
<BoardDynamic />
</>
I have to access the hash from the URL so I come up with this
const hash = global.window && window.location.hash;
Here's an easy-to-use workaround that I did.
const runOnClient = (func: () => any) => {
if (typeof window !== "undefined") {
if (window.document.readyState == "loading") {
window.addEventListener("load", func);
} else {
func();
}
}
};
Usage:
runOnClient(() => {
// access window as you like
})
// or async
runOnClient(async () => {
// remember to catch errors that might be raised in promises, and use the `await` keyword wherever needed
})
This is better than just typeof window !== "undefined", because if you just check that the window is not undefined, it won't work if your page was redirected to, it just works once while loading. But this workaround works even if the page was redirected to, not just once while loading.
I was facing the same problem when i was developing a web application in next.js This fixed my problem, you have to refer to refer the window object in a life cycle method or a react Hook. For example lets say i want to create a store variable with redux and in this store i want to use a windows object i can do it as follows:
let store
useEffect(()=>{
store = createStore(rootReducers, window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__())
}, [])
....
So basically, when you are working with window's object always use a hook to play around or componentDidMount() life cycle method
I wrapped the general solution (if (typeof window === 'undefined') return;) in a custom hook, that I am very pleased with. It has a similiar interface to reacts useMemo hook which I really like.
import { useEffect, useMemo, useState } from "react";
const InitialState = Symbol("initial");
/**
*
* #param clientFactory Factory function similiar to `useMemo`. However, this function is only ever called on the client and will transform any returned promises into their resolved values.
* #param deps Factory function dependencies, just like in `useMemo`.
* #param serverFactory Factory function that may be called server side. Unlike the `clientFactory` function a resulting `Promise` will not be resolved, and will continue to be returned while the `clientFactory` is pending.
*/
export function useClientSideMemo<T = any, K = T>(
clientFactory: () => T | Promise<T>,
deps: Parameters<typeof useMemo>["1"],
serverFactory?: () => K
) {
const [memoized, setMemoized] = useState<T | typeof InitialState>(
InitialState
);
useEffect(() => {
(async () => {
setMemoized(await clientFactory());
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps);
return typeof window === "undefined" || memoized === InitialState
? serverFactory?.()
: memoized;
}
Usage Example:
I am using it to dynamically import libaries that are not compatible with SSR in next.js, since its own dynamic import is only compatible with components.
const renderer = useClientSideMemo(
async () =>
(await import("#/components/table/renderers/HighlightTextRenderer"))
.HighlightTextRendererAlias,
[],
() => "text"
);
As you can see I even implemented a fallback factory callback, so you may provide a result when initially rendering on the server aswell. In all other aspects this hook should behave similiar to reacts useMemo hook. Open to feedback.
For such cases, Next.js has Dynamic Import.
A module that includes a library that only works in the browser, it's suggested to use Dynamic Import. Refer
Date: 06/08/2021
Check if the window object exists or not and then follow the code along with it.
function getSelectedAddress() {
if (typeof window === 'undefined') return;
// Some other logic
}
For Next.js version 12.1.0, I find that we can use process.title to determine whether we are in browser or in node side. Hope it helps!
export default function Projects(props) {
console.log({ 'process?.title': process?.title });
return (
<div></div>
);
}
1. From the terminal, I receive { 'process?.title': 'node' }
2. From Chrome devtool, I revice { 'process?.title': 'browser' }
I had this same issue when refreshing the page (caused by an import that didn't work well with SSR).
What fixed it for me was going to pages where this was occurring and forcing the import to be dynamic:
import dynamic from 'next/dynamic';
const SomeComponent = dynamic(()=>{return import('../Components/SomeComponent')}, {ssr: false});
//import SomeComponent from '../Components/SomeComponent'
Commenting out the original import and importing the component dynamically forces the client-side rendering of the component.
The dynamic import is covered in Nextjs's documentation here:
https://nextjs.org/docs/advanced-features/dynamic-import
I got to this solution by watching the youtube video here:
https://www.youtube.com/watch?v=DA0ie1RPP6g
You can define a state var and use the window event handle to handle changes like so.
const [height, setHeight] = useState();
useEffect(() => {
if (!height) setHeight(window.innerHeight - 140);
window.addEventListener("resize", () => {
setHeight(window.innerHeight - 140);
});
}, []);
You can try the below code snippet for use-cases such as - to get current pathname (CurrentUrl Path)
import { useRouter } from "next/router";
const navigator = useRouter()
console.log(navigator.pathname);
For anyone who somehow cannot use hook (for example, function component):
Use setTimeout(() => yourFunctionWithWindow()); will allow it get the window instance. Guess it just need a little more time to load.
I want to leave this approach that I found interesting for future researchers. It's using a custom hook useEventListener that can be used in so many others needs.
Note that you will need to apply a little change in the originally posted one, like I suggest here.
So it will finish like this:
import { useRef, useEffect } from 'react'
export const useEventListener = (eventName, handler, element) => {
const savedHandler = useRef()
useEffect(() => {
savedHandler.current = handler
}, [handler])
useEffect(() => {
element = !element ? window : element
const isSupported = element && element.addEventListener
if (!isSupported) return
const eventListener = (event) => savedHandler.current(event)
element.addEventListener(eventName, eventListener)
return () => {
element.removeEventListener(eventName, eventListener)
}
}, [eventName, element])
}
If it is NextJS app and inside _document.js, use below:
<script dangerouslySetInnerHTML={{
__html: `
var innerHeight = window.innerHeight;
`
}} />

How do I get the title of the current Router Config Aurelia

I want message to persist through all pages of my application, and be set after the view-model and view are bounded. Every page I have a very simple template.
<template>
<h1>${message}</h1>
</template>
The code behind is empty for each page ('Page1', 'Home', 'Page2').
Here is my app.js
import {inject} from 'aurelia-framework';
export class App {
configureRouter(config, router) {
config.title = 'Pathways';
config.map([
{ route: ['','home'], name: 'home', moduleId: 'home', nav: true, title:'Home' },
{ route: 'page1', name: 'page1', moduleId: 'page1', nav: true, title:'Page1' }
]);
// Create a binding to the router object
this.router = router;
}
attached() {
this.message = this.router.currentInstruction.config.title;
}
}
The problem is, when I first load the app. ${message} is blank. Then, when I navigate to page1, the message becomes Home !! I thought that maybe the currentInstruction was lagging behind, so I added another page. Page2 if you will, but it turns out no matter what I do after the first navigation the message is always Home
So on app initialization message is blank, and then upon the first navigation message will remain Home for the entirety.
My question is, what is the "Aurelia" way to get the title of each page the user is currently on? I would think injecting the router on each page is costly
I can see two options:
The first and easiest one is using a getter property that listens to router.currentInstruction. For instance:
#computedFrom('router.currentInstruction')
get message() {
if (this.router.currentInstruction !== null)
return this.router.currentInstruction.config.title;
}
Running Example https://gist.run/?id=b01abe2859bc2bea1af0f8d21fc2045f
The second option is using the EventAggregator. For example:
attached() {
this.subscription = this.ea.subscribe('router:navigation:success', () => {
this.message = this.router.currentInstruction.config.title;
});
}
detached() {
this.subscription.dispose(); //remember to always dispose the subscription
}
Running example https://gist.run/?id=7d30d4e8d479cc209ca9013e10e91505
They say we should try to avoid the EventAggregator. So, I'd go with the first option.
You can use router.currentInstruction.config.title direct on your app.html page and it will be updated every time then router title is changed (it's using one-way binding):
<h1>title: ${router.currentInstruction.config.title}</h1>
Best way (thanks comments):
${router.currentInstruction.config.navModel.title}

Best practices when editing form with emberjs

Is there any good solutions when editing a form to save the model only if the user clicked on the save button and retrieve the old datas if the user canceled the action ?
I've seen some solutions like duplicating the object that is data-binded with each form fields and set the the initial object with the duplicated one when it is saved.
If you could give answers without using ember data could be great.
I understand you would prefer a solution that doesn't use ember-data, but I would argue that using ember-data is best practices. Here is a solution using ember-data because I imagine a lot of people may come across this question...
If you set up your route as follows, it will do exactly that.
App.CommentEditRoute = Em.Route.extend({
model: function(params) {
return this.store.find('comment', params.comment_id);
},
actions: {
willTransition: function(transition) {
var model = this.get('controller.content');
if (model.get('isDirty')) {
model.rollback();
}
}
},
});
If you call this.get('content').save() in the controller (because the user clicked the save button) it will persist the changes through the adapter and isDirty will be set to false. Thus, the model will not rollback. Otherwise, if you did not call this.get('content').save() in the controller, the isDirty property will be true and the unsaved changes will be discarded. See the DS.Model docs for more info.
willTransition is an event automatically called when the route is about to change - you don't have to call it directly.
Your controller might look like this:
App.CommentEditController = Em.ObjectController.extend({
save: function() {
var _this = this;
_this.get('content').save().then(function() {
// Success
_this.transitionToRoute('comments');
}, function() {
// Failure
Em.assert('Uh oh!');
});
},
cancel: function() {
this.transitionToRoute('comments');
},
});
Also, be sure to utilize the default HTML form submission using a proper HTML button or input for submission so you can capture the submission event in your view as follows:
App.CommentEditView = Em.View.extend({
submit: function() {
this.get('controller').save();
},
});