Could not find "store" in the context of "Connect(withRedux(MyApp))" - reactjs

I am trying to do authentication in Nextjs using firebase and redux but I run into this error when I use connect from react-redux.
Note: I'm not a pro so do forgive me if my code seems bad.
import "../styles/global.scss";
import "bootstrap/dist/css/bootstrap.min.css";
import App from "next/app";
import { Provider, connect } from "react-redux";
import withRedux from "next-redux-wrapper";
import { auth, createUserProfileDocument } from "../firebase/firebase.utils";
import store from "../redux/store";
import { setCurrentUser } from "../redux/user/user.actions";
class MyApp extends App {
static async getInitialProps({ Component, ctx }) {
const pageProps = Component.getInitialProps
? await Component.getInitialProps(ctx)
: {};
//Anything returned here can be access by the client
return { pageProps: pageProps };
}
unsubscribeFromAuth = null;
componentDidMount() {
const { setCurrentUser } = this.props;
this.unsubscribeFromAuth = auth.onAuthStateChanged(async (userAuth) => {
if (userAuth) {
const userRef = await createUserProfileDocument(userAuth);
userRef.onSnapshot((snapshot) => {
setCurrentUser({
id: snapshot.id,
...snapshot.data(),
});
});
} else {
setCurrentUser(userAuth);
}
});
}
componentWillUnmount() {
this.unsubscribeFromAuth();
}
render() {
//Information that was returned from 'getInitialProps' are stored in the props i.e. pageProps
const { Component, pageProps, store } = this.props;
return (
<Provider store={store}>
<Component {...pageProps} />
</Provider>
);
}
}
//makeStore function that returns a new store for every request
const makeStore = () => store;
const mapDispatchToProps = (dispatch) => ({
setCurrentUser: (user) => dispatch(setCurrentUser(user)),
});
const wrappedApp = withRedux(makeStore)(MyApp);
export default connect(null, mapDispatchToProps)(wrappedApp);
I have a sandbox link here.
Please help

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?

NextJS: How to add screen loading for production build?

I want add screen loading in next js project. And I tried to do that with the Router component in next/router.
This is my _app.js in next.js project:
import {CookiesProvider} from 'react-cookie';
import App from 'next/app'
import React from 'react'
import {Provider} from 'react-redux'
import withRedux from 'next-redux-wrapper'
import withReduxSaga from 'next-redux-saga'
import createStore from '../src/redux/store'
import Router from "next/router";
import {Loaded, Loading} from "../src/util/Utils";
class MyApp extends App {
static async getInitialProps({Component, ctx}) {
let pageProps = {};
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps({ctx})
}
return {pageProps}
}
render() {
Router.onRouteChangeStart = () => {
Loading()
};
Router.onRouteChangeComplete = () => {
Loaded()
};
Router.onRouteChangeError = () => {
Loaded()
};
const {Component, pageProps, store} = this.props;
return (
<CookiesProvider>
<Provider store={store}>
<Component {...pageProps} />
</Provider>
</CookiesProvider>
)
}
}
export default withRedux(createStore)(withReduxSaga(MyApp))
This is Loaded() and Loading() functions:
export const Loaded = () => {
setTimeout(() => {
let loading = 'has-loading';
document.body.classList.remove(loading);
}, 100);
};
export const Loading = () => {
let loading = 'has-loading';
document.body.classList.add(loading);
};
The code works well when the project is under development mode. But when the project is built, the loading won't disappear.
Do you know solution of this issue or are you suggesting another solution?
Using apollo client and react hooks you could do as follow.
Example:
import { useQuery } from '#apollo/react-hooks';
import gql from 'graphql-tag';
import { withApollo } from '../lib/apollo';
import UserCard from '../components/UserCard';
export const USER_INFO_QUERY = gql`
query getUser ($login: String!) {
user(login: $login) {
name
bio
avatarUrl
url
}
}
`;
const Index = () => {
const { query } = useRouter();
const { login = 'default' } = query;
const { loading, error, data } = useQuery(USER_INFO_QUERY, {
variables: { login },
});
if (loading) return 'Loading...'; // Loading component
if (error) return `Error! ${error.message}`; // Error component
const { user } = data;
return (
<UserCard
float
href={user.url}
headerImg="example.jpg"
avatarImg={user.avatarUrl}
name={user.name}
bio={user.bio}
/>
);
};
export default withApollo({ ssr: true })(Index);
More info here: https://github.com/zeit/next.js/tree/canary/examples/with-apollo
I added the following codes to a wrapper component and the problem was resolved.
componentDidMount() {
Loaded();
}
componentWillUnmount() {
Loading();
}

Unable to call props functions in componentDidMount

From Party.Container where is connected Party with mapStateToProps and mapDispatchToProps, are sent two functions to Party (fetchData and fetchFooter)
They worked until I implemented in project eslint:"airbnb", and now it's constantly getting this error "Must use destructuring props assignment react/destructuring-assignment".
const mapActionsToProps = {
fetchData,
fetchDataFooter,};
--- these are functions
componentDidMount() {
this.props.fetchData();
this.props.fetchDataFooter(); }
This is the component
import { connect } from 'react-redux';
import { fetchData, fetchDataFooter } from './actions';
import Party from './Party';
const mapStateToProps = state => ({
wishlist: state.wishlist,
cart: state.cart,
});
const mapActionsToProps = {
fetchData,
fetchDataFooter,
};
export default connect(mapStateToProps, mapActionsToProps)(Party);
This is COntainer
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Header from '../../components/Header/Header';
import Content from './Content/Content.Container';
import styles from './Party.module.scss';
import Footer from '../../components/Footer/Footer';
const propTypes = {
wishlist: PropTypes.shape.isRequired,
cart: PropTypes.shape.isRequired,
// fetchData: PropTypes.func.isRequired,
// fetchDataFooter: PropTypes.func.isRequired,
};
class Party extends Component {
componentDidMount() {
// this.props.fetchData();
// this.props.fetchDataFooter();
}
render() {
const { wishlist, cart } = this.props;
let name;
let profilePicture;
let personWishlist;
let purchases;
let id;
if (wishlist.isFulfilled === true) {
const listId = wishlist.payloadData.data.filter(x => x.id === 1);
({ name } = listId[0].name);
({ profilePicture } = listId[0].picture);
({ personWishlist } = listId[0].wishList);
({ purchases } = listId[0].purchases);
({ id } = listId[0].id);
}
console.log(wishlist, cart);
return (
<div className={styles.Party}>
<Header />
<Content
name={name}
id={id}
profilePicture={profilePicture}
personWishlist={personWishlist}
purchases={purchases}
/>
<Footer
cart={cart}
/>
</div>
);
}
}
Party.propTypes = propTypes;
export default Party;
Can you try the one in below in your componentDidMount method as the error suggests:
componentDidMount() {
const { fetchData, fetchDataFooter } = this.props;
fetchData();
fetchDataFooter();
}
Actually, it means that your expressions should be destructured before usage.
E.g.: you're using:
...
this.props.fetchData();
this.props.fetchDataFooter();
...
You have to change it to:
const { fetchData, fetchDataFooter } = this.props;
fetchData();
fetchDataFooter();
Another solution is to disable this if you want to in your rules file.
"react/destructuring-assignment": [<enabled>, 'always'] - can be always or never.
See here for more information.

Resources