How to persist Mobx data on NextJS - reactjs

Problem
After user login user details are saved in mobx state. but after page refresh those data are gone. I want to persist data on mobx state so the user data are stay on mobx state until user logout.
I tried mobx-persist-store but when I go to another page those data are gone again. How can I implement persistence store with or without mobx-persist-store in Next.js?
root-store-provider
import { createContext, FC, useContext } from "react";
import { RootStore } from "./root";
const RootStoreContext = createContext<RootStore>(new RootStore());
export const useStore = () => {
const context = useContext(RootStoreContext);
if (context === undefined) {
throw new Error("useStore must be used within StoreProvider");
}
return context;
};
let rootStore: RootStore | undefined;
const initializeStore = (initialData?: RootStore) => {
const _rootStore = rootStore ?? new RootStore();
if (initialData) {
_rootStore.hydrate(initialData);
}
if (typeof window === "undefined") return _rootStore;
if (!rootStore) rootStore = _rootStore;
return _rootStore;
};
interface Props {
children: React.ReactNode;
intialState?: RootStore;
}
export const RootStoreProvider: FC<Props> = ({ children, intialState }) => {
const store = initializeStore(intialState);
return (
<RootStoreContext.Provider value={store}>
{children}
</RootStoreContext.Provider>
);
};
rootStore
import { UserStore } from "./user";
import { hydrateStore, makePersistable } from "mobx-persist-store";
import localforage from "localforage";
export class RootStore {
user: UserStore;
constructor() {
this.user = new UserStore(this);
makePersistable(
this.user,
{
name: "RootStore",
properties: ["details", "cart"],
storage: typeof window !== "undefined" ? localforage : undefined,
},
{ delay: 200, fireImmediately: false }
);
}
async hydrate(data: RootStore): Promise<void> {
await hydrateStore(data);
}
}

Related

React context not updating from after promise completed

I have a React context which I am using to manage the authentication within my application. I have done this previously and all seemed OK, but in this application the value of the isAuthenticated property is not being updated. I've tried to replicate using CodeSanbox but I get the expected result.
Essentially, I want the context to hold a value of isAuthenticating: true until the authentication flow has finished, once this has finished I will determine if the user is authenticated by checking isAuthenticated === true && authenticatedUser !== undefined however, the state does not seem to be getting updated.
As a bit of additional context to this, I am using turborepo and next.js.
AuthenticationContext:
import { SilentRequest } from '#azure/msal-browser';
import { useMsal } from '#azure/msal-react';
import { User } from 'models';
import { createContext, useCallback, useContext, useEffect, useState } from "react";
import { msal, sendRequest } from 'utils';
interface AuthenticationContextType {
authenticatedUser?: User;
isAuthenticating: boolean;
}
const AuthenticationContext = createContext<AuthenticationContextType>({
authenticatedUser: undefined,
isAuthenticating: true
});
export const AuthenticationProvider = (props: { children: React.ReactNode }) => {
const { accounts, instance } = useMsal();
const [user, setUser] = useState<User>();
const [isAuthenticating, setIsAuthenticating] = useState<boolean>(true);
const [currentAccessToken, setCurrentAccessToken] = useState<string>();
const getUserFromToken = useCallback(async () => {
if (user) {
setIsAuthenticating(false);
return;
}
const userRequest = await sendRequest('me');
if (! userRequest.error && userRequest.data) {
setUser(userRequest.data as User);
}
}, [user]);
const getAccessToken = useCallback(async () => {
if (! currentAccessToken) {
const request: SilentRequest = {
...msal.getRedirectRequest(),
account: accounts[0]
}
const response = await instance.acquireTokenSilent(request);
setCurrentAccessToken(response.accessToken);
}
return getUserFromToken();
}, [accounts, currentAccessToken, getUserFromToken, instance]);
useEffect(() => {
async function initialiseAuthentication() {
await getAccessToken();
setIsAuthenticating(false);
}
initialiseAuthentication();
}, [getAccessToken]);
return (
<AuthenticationContext.Provider value={{ authenticatedUser: user, isAuthenticating }}>
{ props.children }
</AuthenticationContext.Provider>
)
}
export const useAuth = () => {
const context = useContext(AuthenticationContext);
if (context === undefined) {
throw new Error("useAuth was used outside of it's provider.")
}
return context;
}
AuthenticationLayout:
import { useEffect, useState } from 'react';
import { AuthenticationProvider, useAuth } from '../hooks/authentication';
import MsalLayout from './msal-layout';
const AuthenticationLayout = (props: { children: React.ReactNode }) => {
const { isAuthenticating, authenticatedUser } = useAuth();
const wasAuthenticationSuccessful = () => {
return ! isAuthenticating && authenticatedUser !== undefined;
}
const renderContent = () => {
if (! wasAuthenticationSuccessful()) {
return (
<p>You are not authorized to view this application.</p>
)
}
return props.children;
}
if (isAuthenticating) {
return (
<p>Authenticating...</p>
)
}
return (
<MsalLayout>
{ renderContent() }
</MsalLayout>
)
}
export default AuthenticationLayout;
MsalLayout:
import { InteractionType } from '#azure/msal-browser';
import {
AuthenticatedTemplate,
MsalAuthenticationTemplate,
MsalProvider,
} from "#azure/msal-react";
import { msalInstance, msal } from 'utils';
import { AuthenticationProvider } from '../hooks/authentication';
msal.initialize();
const MsalLayout = (props: { children: React.ReactNode }) => {
return (
<MsalProvider instance={msalInstance}>
<MsalAuthenticationTemplate interactionType={InteractionType.Redirect} authenticationRequest={msal.getRedirectRequest()}>
<AuthenticatedTemplate>
<AuthenticationProvider>
{props.children}
</AuthenticationProvider>
</AuthenticatedTemplate>
</MsalAuthenticationTemplate>
</MsalProvider>
)
}
export default MsalLayout;
Theoretically, once the authentication is finished I would expect the props.children to display.
I think that the problem is AuthenticationLayout is above the provider. You have consumed the provider in MsalLayout. Then AuthenticationLayout uses MsalLayout so the AuthenticationLayout component is above the provider in the component tree. Any component that consumes the context, needs to be a child of the provider for that context.
Therefore the context is stuck on the static default values.
Your capture of this scenario in useAuth where you throw an error is not warning you of this as when its outside the context -- context is not undefined, it is instead the default values which you pass to createContext. So your if guard isn't right.
There are some workarounds to checking if its available -- for example you could use undefined in the default context for isAuthenticating and authenticatedUser and then check that. Or you can change them to getters and set the default context version of this function such that it throws an error.

How to set the default react context value as data from firestore?

I'm building a workout program planner app, the workout program is handled in the app with a SetProgram context and is updated with a custom hook called useProgram. I need that when the user logins that the app will fetch data from firestore and display the user's workout program, how can I do this? Keeping in mind that the useProgram hook is also used throughout the app to edit and update one's workout program.
App.tsx
import React, { useContext, useEffect, useState } from "react";
import { BrowserRouter as Router } from "react-router-dom";
import AppRouter from "./Router";
import FirebaseApp from "./firebase";
import SetProgram from "./context/program";
import { useProgram } from "./components/hooks/useProgram";
import firebaseApp from "./firebase/firebase";
import { useAuthState } from "react-firebase-hooks/auth";
function App() {
const program = useProgram();
const day = useDay();
const [user, loading, error] = useAuthState(firebaseApp.auth);
return (
<div className="App">
<SetProgram.Provider value={program}>
<Router>
<AppRouter />
</Router>
</SetProgram.Provider>
</div>
);
}
export default App;
firebase.ts
import firebase from "firebase/app";
import "firebase/auth";
import "firebase/firestore";
import firebaseConfig from "./config";
class Firebase {
auth: firebase.auth.Auth;
user: firebase.User | null | undefined;
db: firebase.firestore.Firestore;
userProgram: {} | undefined;
constructor() {
firebase.initializeApp(firebaseConfig);
this.auth = firebase.auth();
this.db = firebase.firestore();
}
async register() {
if (this.user) {
this.db.collection("users").doc(this.user.uid).set({
name: this.user.displayName,
email: this.user.email,
userId: this.user.uid,
program: {},
});
}
}
async getResults() {
return await this.auth.getRedirectResult().then((results) => {
console.log("results.user", results.user);
if (!results.additionalUserInfo?.isNewUser) {
this.getProgram();
} else {
this.register();
}
});
}
async login(
user: firebase.User | null | undefined,
loading: boolean,
error: firebase.auth.Error | undefined
) {
const provider = new firebase.auth.GoogleAuthProvider();
return await this.auth
.signInWithRedirect(provider)
.then(() => this.getResults());
}
async logout() {
return await this.auth.signOut().then(() => console.log("logged out"));
}
async updateProgram(user: firebase.User, program: {}) {
if (this.userProgram !== program) {
firebaseApp.db
.collection("users")
.doc(user.uid)
.update({
program: program,
})
.then(() => console.log("Program updated successfully!"))
.catch((error: any) => console.error("Error updating program:", error));
} else {
console.log("No changes to the program!");
}
}
async getProgram() {
firebaseApp.db
.collection("users")
.doc(this.user?.uid)
.get()
.then((doc) => {
console.log("hello");
if (doc.exists) {
this.userProgram = doc.data()?.program;
console.log("this.userProgram", this.userProgram);
} else {
console.log("doc.data()", doc.data());
}
});
}
}
const firebaseApp = new Firebase();
export default firebaseApp;
programContext.tsx
import React from "react";
import Program, { muscleGroup, DefaultProgram } from "../interfaces/program";
export interface ProgramContextInt {
program: Program | undefined;
days: Array<[string, muscleGroup]> | undefined;
setProgram: (p: Program) => void;
}
export const DefaultProgramContext: ProgramContextInt = {
program: undefined,
days: undefined,
setProgram: (p: Program): void => {},
};
const ProgramContext = React.createContext<ProgramContextInt>(
DefaultProgramContext
);
export default ProgramContext;
useProgram.tsx
import React from "react";
import {
ProgramContextInt,
DefaultProgramContext,
} from "../../context/program";
import Program, { muscleGroup } from "../../interfaces/program";
import { useAuthState } from "react-firebase-hooks/auth";
import firebaseApp from "../../firebase";
export const useProgram = (): ProgramContextInt => {
const [user] = useAuthState(firebaseApp.auth);
const [program, setEditedProgram] = React.useState<Program | undefined>();
const [days, setProgramDays] = React.useState<
[string, muscleGroup][] | undefined
>(program && Object.entries(program));
const setProgram = React.useCallback(
(program: Program): void => {
firebaseApp.updateProgram(user, program);
setEditedProgram(program);
setProgramDays(Object.entries(program));
},
[user]
);
return {
program,
days,
setProgram,
};
};
There are two ways to handle this in my opinion:
Update the ProgramContext to make sure that the user is logged in
Wrap the App or any other entry point from whence you need to make sure that the user is logged in, in a separate UserContextProvider
Let's talk about the latter method, where we can wrap in a separate context called UserContext. Firebase provides us a listener called onAuthStateChanged, which we can make use of in our context, like so:
import { createContext, useEffect, useState } from "react";
import fb from "services/firebase"; // you need to define this yourself. It's just getting the firebase instance. that's all
import fbHelper from "services/firebase/helpers"; // update path based on your project organization
type FirestoreDocSnapshot = firebase.default.firestore.DocumentSnapshot<firebase.default.firestore.DocumentData>;
const UserContext = createContext({ user: null, loading: true });
const UserContextProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const userContext = { user, loading };
const updateUser = (snapShot: FirestoreDocSnapshot) => {
setUser({
id: snapShot.id,
...snapShot.data,
});
};
const authStateListener = async (authUser: firebase.default.User) => {
try {
if (!authUser) {
setUser(authUser);
return;
}
const fbUserRef = await fbHelper.findOrCreateFirestoreUser(authUser)
if ("error" in fbUserRef) throw new Error(fbUserRef?.error);
(fbUserRef as FirestoreUserRef).onSnapshot(updateUser)
} catch (error) {
throw error;
} finally {
setLoading(false);
}
};
useEffect(() => {
const unSubscribeAuthStateListener = fb.auth.onAuthStateChanged(authStateListener);
return () => unSubscribeAuthStateListener();
}, [])
return (
<UserContext.Provider value={userContext}>
{children}
</UserContext.Provider>
)
};
export default UserContextProvider;
Where the helper can be something like this:
export type FirestoreUserRef = firebase.default.firestore.DocumentReference<firebase.default.firestore.DocumentData>
const findOrCreateFirestoreUser = async (authUser: firebase.default.User, additionalData = {}): Promise<FirestoreUserRef | { error?: string }> => {
try {
if (!authUser) return { error: 'authUser is missing!' };
const user = fb.firestore.doc(`users/${authUser.uid}`); // update this logic according to your schema
const snapShot = await user.get();
if (snapShot.exists) return user;
const { email } = authUser;
await user.set({
email,
...additionalData
});
return user;
} catch (error) {
throw error;
}
};
Then wrap your other context which provides firestore data, within this UserContextProvider. Thus whenever you login or logout, this particular listener be invoked.

Context evaluating as undefined in React Native app

I'm attempting to create an Auth context file which, upon app load, checks if a user is signed in.
To do this, I'm using a 'helper' function which allows me to import the initialisation of the context and just build upon that with additional functions which authorise a user.
However, upon every app load, the Context is returning as 'undefined', and it says 'evaluating _useContext.trySignIn'.
For reference, here is my Context file:
import createDataContext from './createDataContext';
import { AsyncStorage } from 'react-native';
import { navigate } from '../navigationRef';
import { Magic } from '#magic-sdk/react-native';
const m = new Magic('API_key');
const authReducer = (state, reducer) => {
switch (action.type) {
default:
return state;
}
};
const trySignIn = dispatch => async () => {
const isLoggedIn = await m.user.isLoggedIn();
if (isLoggedIn === true) {
navigate('Dashboard');
} else {
navigate('loginFlow');
}
};
export const { Provider, Context } = createDataContext (
authReducer,
{ trySignIn },
{ isLoggedIn: null }
);
Here is my 'createDataContext' file:
import React, { useReducer } from 'react';
export default (reducer, actions, defaultValue) => {
const Context = React.createContext();
const Provider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, defaultValue);
const boundActions = {};
for (let key in actions) {
boundActions[key] = actions[key].dispatch;
}
return (
<Context.Provider value={{ state, ...boundActions }}>
{children}
</Context.Provider>
)
};
return { Context, Provider }
};
Here is my navigation file:
import { NavigationActions } from 'react-navigation';
let navigator;
export const setNavigator = (nav) => {
navigation = nav;
};
export const navigate = (routeName, params) => {
navigator.dispatch(
NavigationActions.navigate({
routeName, params
})
);
};
And finally, here is my component attempting to use my context:
import React, { useEffect, useContext } from 'react';
import { View, Text, StyleSheet, ActivityIndicator } from 'react-native';
import { Context } from '../context/AuthContext';
const LoadingScreen = () => {
const { trySignIn } = useContext(Context);
useEffect(() => {
trySignIn();
}, [])
return (
<View style={styles.mainView}>
<ActivityIndicator style={styles.indicator} />
</View>
)
}
Can anyone see why my context would be returning as 'undefined' in my component?

MobX and Next.js (Fast Refresh)

I can't seem to find a way to set initial checkbox state from MobX store after a fast refresh from Next.js. As soon as i refresh the page, it renders with the state i set before.
For example: i check the checkbox (which has the checked/onChange routed to MobX) -> Page refresh -> The input persists to be checked, while the state is set to false.
I've tried all the other ways to pass the observer HOC (Observer, useObserver), disabling hydration, reworking store, but to no avail.
Here's the code:
store/ThemeStore.ts
import { makeAutoObservable } from 'mobx';
export type ThemeHydration = {
darkTheme: boolean;
};
class ThemeStore {
darkTheme = false;
constructor() {
makeAutoObservable(this);
}
setDarkTheme(value: boolean) {
this.darkTheme = value;
}
hydrate(data?: ThemeHydration) {
if (data) {
this.darkTheme = data.darkTheme;
}
}
}
export default ThemeStore;
pages/index.tsx
import React, { useEffect } from "react";
import { reaction } from "mobx";
import styles from "#/styles/homepage.module.scss";
import { observer } from "mobx-react";
import { useStore } from "#/stores";
const HomePage = observer(function () {
const { themeStore } = useStore();
useEffect(() => {
const re = reaction(
() => themeStore.darkTheme,
(value) => {
const body = document.body;
if (value) {
body.classList.remove("theme-light");
body.classList.add("theme-dark");
} else {
body.classList.remove("theme-dark");
body.classList.add("theme-light");
}
},
{ fireImmediately: true }
);
return () => {
re();
};
}, []);
return (
<div className={styles.container}>
<main className={styles.main}>
<input
type="checkbox"
defaultChecked={themeStore.darkTheme}
onChange={(e) => {
themeStore.setDarkTheme(e.target.checked);
}}
/>
</main>
</div>
);
});
export default HomePage;
stores/index.tsx
import React, { ReactNode, createContext, useContext } from "react";
import { enableStaticRendering } from "mobx-react";
import RootStore, { RootStoreHydration } from "./RootStore";
enableStaticRendering(typeof window === "undefined");
export let rootStore = new RootStore();
export const StoreContext = createContext<RootStore | undefined>(undefined);
export const useStore = () => {
const context = useContext(StoreContext);
if (context === undefined) {
throw new Error("useRootStore must be used within RootStoreProvider");
}
return context;
};
function initializeStore(initialData?: RootStoreHydration): RootStore {
const _store = rootStore ?? new RootStore();
if (initialData) {
_store.hydrate(initialData);
}
// For SSG and SSR always create a new store
if (typeof window === "undefined") return _store;
// Create the store once in the client
if (!rootStore) rootStore = _store;
return _store;
}
export function RootStoreProvider({
children,
hydrationData,
}: {
children: ReactNode;
hydrationData?: RootStoreHydration;
}) {
const store = initializeStore(hydrationData);
return (
<StoreContext.Provider value={store}>{children}</StoreContext.Provider>
);
}
Thanks in advance!

how can i dispatch store and override initial props in nextjs

I want to call API inside getInitialProps then save that response in redux store.so how can I call dispatch store and override initial props
right now after fetching data from API, I'm able to dispatch value to the reducer and it also saving data in store but after saving data my app calling for initial props(i don't know from where it changes the states) and overriding my new saved data into initial props.
main.js
class StepThree extends React.Component {
static async getInitialProps({ reduxStore, req }) {
let encID = req.query.id //null
try {
let encode = await encrption(encID,7,'dec')
const apiCall = await fetch(`${config.leadSearchApi}&search_param=${encode}`);
let res = await apiCall.json();
if(res.data['next_action'] !== "converted"){
let test = await reduxStore.dispatch({ type: PATIENT_NAME,payload:res.data.name });
console.log(test,'res');
await reduxStore.dispatch({ type: PATIENT_NUMBER,payload:res.data['mobile_number'] });
await reduxStore.dispatch({ type: LEAD_ID,payload:res.data.id });
}
} catch (err) {
console.log(err,'get err');
}
return { }
}
render() {
return <div>Hello World </div>
}
}
const mapStateToProps = (state, prevProps) =>{
return{
AmbSelect:state.StepThreeReducer.isAmbSel,
Addons:state.StepThreeReducer.addonSel,
VehcileData:state.StepThreeReducer.vehcileData,
TotalPrice:state.StepThreeReducer.totalPrice,
Cameback:state.StepThreeReducer.comeback,
LeadID:state.Main.LeadId
}
}
export default connect(mapStateToProps,{addOnSelected,priceCal,updateLeadS3,previous,VehicleDataHandler,updateVehData, addonsCount,totalPrice,existLeadData,linkLead2,linkLead3,encrption })(StepThree);
App.js
import App, { Container } from 'next/app'
import React from 'react'
import withReduxStore from '../lib/with-redux-store'
import { Provider } from 'react-redux'
class MyApp extends App {
render () {
const { Component, pageProps, reduxStore } = this.props;
return (
<Container>
<Provider store={reduxStore}>
<Component {...pageProps} />
</Provider>
</Container>
)
}
}
export default withReduxStore(MyApp)
redux-store.js
import React from 'react'
import { initializeStore } from '../store'
const isServer = typeof window === 'undefined'
const __NEXT_REDUX_STORE__ = '__NEXT_REDUX_STORE__'
function getOrCreateStore (initialState) {
// Always make a new store if server, otherwise state is shared between requests
if (isServer) {
return initializeStore(initialState)
}
// Create store if unavailable on the client and set it on the window object
if (!window[__NEXT_REDUX_STORE__]) {
window[__NEXT_REDUX_STORE__] = initializeStore(initialState)
}
return window[__NEXT_REDUX_STORE__]
}
export default App => {
return class AppWithRedux extends React.Component {
static async getInitialProps (appContext) {
// Get or Create the store with `undefined` as initialState
// This allows you to set a custom default initialState
const reduxStore = getOrCreateStore()
// Provide the store to getInitialProps of pages
appContext.ctx.reduxStore = reduxStore
let appProps = {}
if (typeof App.getInitialProps === 'function') {
appProps = await App.getInitialProps(appContext)
}
return {
...appProps,
initialReduxState: reduxStore.getState()
}
}
constructor (props) {
super(props)
this.reduxStore = getOrCreateStore(props.initialReduxState)
}
render () {
return <App {...this.props} reduxStore={this.reduxStore} />
}
}
}
store.js
import { createStore, applyMiddleware,combineReducers } from 'redux'
import { composeWithDevTools } from 'redux-devtools-extension'
import thunkMiddleware from 'redux-thunk'
import './actions';
import reducer from './reducers'
export function initializeStore () {
return createStore(
reducer,
composeWithDevTools(applyMiddleware(thunkMiddleware))
)
}
reducer.js
import {PATIENT_NAME,PATIENT_NUMBER,RIDE_DATE,RIDE_TIME,FCAUSES,SETCAUSE} from '../actions/types';
const INITIAL_STATE = {
patient_name:'',
patient_number:'',
ride_date:false,
ride_time:false,
causes:{},
sel_cause:''
};
export default (state=INITIAL_STATE,action) => {
console.log(action,'reducer')
switch(action.type) {
case PATIENT_NAME:
return {...state,patient_name:action.payload};
case PATIENT_NUMBER:
return {...state,patient_number:action.payload};
case RIDE_DATE:
return {...state,ride_date:action.payload};
case RIDE_TIME:
return {...state,ride_time:action.payload};
case FCAUSES:
return {...state,causes:action.payload};
case SETCAUSE:
return {...state,sel_cause:action.payload};
default:
return state;
}
}
after dispatch I don't want to make app state as initial props
please help,stuckkkkkk
You did't provide an initialState into initializeStore when you creating your store.
To make sure that you're using the same state shape across server and client pass the initialState params to you store initialization inside store.js:
export function initializeStore(initialState = {}) {
return createStore(
reducer,
initialState,
composeWithDevTools(applyMiddleware(thunkMiddleware))
)
}
Otherwise same state will always be applied and server-propagated state will be lost.

Resources