React Native: where to place global state variables - reactjs

I am developing my first React Native, and I need again some help.
My application has one state - sport, which will be important for components, screens and etc. Accordingly the chosen sport, I will load different styles, images, and api information too.
There will be one modal, from which the user can change the sport. The modal now is part of the Header component, which is part of the Screen component.
So my question is how or where to place this sport state variable, so I can access it everywhere and on a change to update the new styles and etc.
The overview of the application is like this:
App.js
import AppContext from './utility/context';
export default function App() {
const [sport, setSport] = React.useState('soccer');
const state = {sport, setSport};
return (
<AppContext.Provider sport={state}>
<OfflineNotice />
<Screen />
</AppContext.Provider>
);
}
context.js
import React from "react";
export const AppContext = React.createContext({
sport: 'soccer',
setSport: () =>{}
});
Screen.js
export default function Screen ({children}) {
return (
<>
<Header />
<SafeAreaView style={styles.screen}>
<View style={styles.container}>{ children }</View>
</SafeAreaView>
<TabNavigator i18n={i18n}/>
</>
);
}
In Header.js I will also use that future state, but at the moment there is nothing interesting.
But here will be the View, from which the user will change the sport state variable.
HomeScreen.js - it is the first screen of the TabNavigator
export default function HomeScreen({ navigation }) {
const today = Moment(new Date().getTime()).format('YYYY-MM-DD');
const [predictions, setPredictions] = useState([]);
const [loading, setLoading] = useState(true);
const params = {
lang: 'en',
date: today,
sport: 'soccer',
};
...
}
Here the sport state is hardcoded because I don't know yet how to proceed.
I've heard about Redux, but I haven't used it yet, so I will appreciate if there is any solution not using Redux.
Thank you in advance!

You can achieve this using React-Context
You can simply have a state in the app.js and use context to access it anywhere you need in the app.
First you need to create the context. Its better if its a separate file
const AppContext = React.createContext({sport:'value',setSport=()=>{}});
Here the default values are options but preferred specially when you use typescript to avoid warnings.
Now you have to use this in your app.js like below
export default function App() {
const [sport,setSport] = React.useState('value');
const state={sport,setSport};
...
return (
<AppContext.Provider value={state}>
<OfflineNotice />
<Screen />
</AppContext.Provider>
);
}
You will have to import the context and use the provider as the wrapper setting the value from the local state that you have. Now you can access this anywhere in the tree and modify it if required.
// Accessing the context using the useContext hook, this component should be in the tree and you should import AppContext
const {sport,setSport} = useContext(AppContext);
You can show it like below
<Text>{sport}</Text>
Or set it like below
<Button title="Set Value" onPress={()=>{setSport('value')}}>
This example is just on a string but you can even have an object.

Related

React Native: Rendered fewer hooks than expected

I'm currently trying to develop an app with multiple screens. Specifically, I'm working on the navigator component that directs the user to the login screen or the home screen based on whether they are logged in or not.
To do this, I'm making use of hooks, React Navigation and Firebase. I have a state which tracks the user, and this state is updated using onAuthStateChanged() from Firebase, which is inside a useEffect hook.
import { useState, useEffect } from 'react';
import { NavigationContainer } from '#react-navigation/native';
import { createNativeStackNavigator } from '#react-navigation/native-stack';
import {
HomeScreen,
LoginScreen,
TimerScreen
} from '../screens';
import { auth } from '../firebase';
import { onAuthStateChanged } from 'firebase/auth';
const MainStack = createNativeStackNavigator();
const AppNavigator = () => {
const [user, setUser] = useState(null);
useEffect(() => {
const subscriber = onAuthStateChanged(auth, authUser => {
if (authUser) {
setUser(authUser);
} else {
setUser(null);
}
});
return subscriber;
});
const MainNavigator = () => (
...
);
return (
<NavigationContainer>
{ user ? MainNavigator() : LoginScreen() }
</NavigationContainer>
);
};
export default AppNavigator;
AppNavigator is then called in my App.js:
export default function App() {
return (
<View style={styles.container}>
<StatusBar style="auto" />
<AppNavigator />
</View>
);
}
However, whenever I run the app, I get
Error: Rendered fewer hooks than expected. This may be caused by an accidental early return statement.
I've read a few posts with the same error message, and a common recommendation is to avoid having hooks inside conditional statements / loops. I did check that my useState and useEffect were at the top level of my component, so that doesn't seem to be the issue.
Right now I'm thinking that the problem could be arising because I'm navigating between screens, but I'll have to look more into it though.
Does anyone know what might be the issue, or any other possible fixes I could try? Any help would be great. Thanks!
user ? MainNavigator() : LoginScreen()
You are calling components as regular functions instead of creating elements from them. To create an element, use the JSX syntax, i.e.:
user ? <MainNavigator /> : <LoginScreen />
(Which will then be transpiled to React.createElement.)
The error occurs because when calling these components as functions, the code inside becomes a part of the render phase of the AppNavigator component. If, for example, MainNavigator contains hooks, and LoginScreen does not, then toggling between which function is (incorrectly) called also changes the number of hooks rendered, as suggested in the error message.

Data-fetching with React Server Components: is this a correct implementation?

I'm using Next.JS in an application where I have a Navbar component that is needed on all pages of the application, and the Navbar renders data (specifically product-categories) that must be fetched from my database.
I want this component to be server-side rendered, but since Next.JS only supports page-level SSR and not component-level SSR, it seems that I must use getServerSideProps on all pages that I want to display this Navbar and write the same API request in each one, resulting in a ton of repeated logic and API calls across several pages.
While researching how to solve this, I came across React Server Components and wonder if that would be a valid solution for this scenario, but since I'm new to the concept, I'm not sure if I understand it correctly, hence why I'm writing this question.
I'm thinking of doing the following and want to get some feedback as to whether I am on the right track.
My Navbar component would be something like as follows:
const Navbar = () => {
const [data, setData] = useState();
useEffect(() => {
fetchData();
}, []);
const fetchData = async () => {
const data = await fetch("/api/someEndpoint");
setData(data);
};
return (
<div>
{data.someProperty}
{data.someOtherProperty}
</div>
);
};
export default Navbar;
Then I can create a global Layout component so that the Navbar is included in all pages, which is where React.Suspense would be used (if I understand it correctly):
const Layout = ({ children }) => {
return (
<>
<React.Suspense fallback={<FallbackComponent />}>
<Navbar />
<React.Suspense />
{children}
</>
);
};
export default Layout;
Then in _app.tsx, I would include my Layout component so that the Navbar is present everywhere:
function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
return (
<Layout>
<Component {...pageProps} />
</Layout>
);
}
So here is my question: is this a correct implementation and valid use-case of React.Suspense? Is component-level data fetching one of the proposed benefits of React Server Components?
Now with the Next.JS 13 version, you can create an app directory that uses react server components by default.
But you should keep in mind that most of the React hooks like useState and useEffect won't work on server components, also previous Next.js APIs such as getServerSideProps, getStaticProps, and getInitialProps are not supported in the new app directory.
So instead, you can use the new fetch() API that was built on top of the native fetch() Web API. Check the docs for more details.
Instead of fetching via an API, you could instead use SSR in _app.tsx that would pass the data as props to the MyApp hook. Then the data could be passed down further into the Layout component, which would again pass it down even further to the Navbar component. So it would look like something along the lines of this:
// _app.tsx
function MyApp({ /* Props */, data }) {
return (
<Layout data={data}>
<Component {...pageProps} />
</Layout>
);
}
export function getServerSideProps(context) {
// Get data
return {
props: {
data: data
},
}
}
// Layout.tsx
const Layout = ({ children, data }) => {
return (
<>
<Navbar data={data} />
{children}
</>
);
};
export default Layout;
// Navbar.tsx
const Navbar = ({ data }) => {
return (
<div>
{data.someProperty}
{data.someOtherProperty}
</div>
);
};
export default Navbar;
EDIT: SSR in _app.tsx doesn't work, but I found an answer here: https://stackoverflow.com/a/72325973/12190941
EDIT 2:
You can use getInitalProps in _app.js if you want your entire site to be server side rendered. Do keep in mind that this will increase initial load times. I found this example in the Next.js docs here
// _app.js
// ...
MyApp.getInitialProps = async (appContext) => {
// calls page's `getInitialProps` and fills `appProps.pageProps`
const appProps = await App.getInitialProps(appContext);
return { ...appProps }

dispatch is not accessible from useContext

i have simple store
import React, { createContext, useReducer } from "react";
import Reducer from "./UserReducer";
const initialState = {
user: {},
error: null
};
const Store = ({ children }) => {
const [state, dispatch] = useReducer(Reducer, initialState);
return (
<Context.Provider value={[state, dispatch]}>
{children}
</Context.Provider>
);
};
export const Context = createContext(initialState);
export default Store;
i have wrapped my app with it like
<Store>
<ThemeProvider theme={Theme}>
<CssBaseline />
<Navbar />
<Wrapper>
<Profile />
</Wrapper>
</ThemeProvider>{" "}
</Store>
There is additional setup as well where my authentication pages are located in separate wrapper so i wrapped that with store as well.
here is code for that wrapper (extra removed).
import Store from "../../../context/Store";
export default function Wrapper({ children }) {
const classes = useStyles();
return (
<Store>
//different dumb containers opening
{children}
//different dumb containers closing
</Store>
);
}
Now when i try to access context within child component like
import React, { useContext } from "react";
import { Context } from "../../../context/Store";
import { SET_USER } from "../../../context/UserTypes";
function SignUp(props) {
const [state, setState] = React.useState({ ...initialState });
const [userData, dispatch] = useContext(Context);
console.log(userData, dispatch, "check");
// rest of component
i get following error
TypeError: undefined is not a function
i tried to log result of useContext without destructuring it but all it had was global state but no dispatch function with it.
Reactjs version = 17.0.1
Update: dispatch is accessible outside withAuthenticator HOC but not within that so it might be amplify issue.
i have opened issue on amplify repo.
Unable to access dispatch from useContext from components wrapped withAuthenticator
There a few things I see that could be potential issues.
The major problem is the value of your Context. When you create the context, its value is a state (createContext(initialState)). But when you pass a value to the Context.Provider you are giving it an array which contains both a state and a dispatch function (value={[state, dispatch]}). You need to be consistent about the value that is contained in the context. Is it a state object or a tuple?
The error that you are describing seems like what would happen if the Context is accessed outside of a Provider. It would fall back the initial context value which is just a state rather than a [state, dispatch] tuple.
You need to change the initial value to something that matches the value that you are expecting.
const Context = createContext([
initialState, // state
() => console.error("Cannot dispatch because Context was used outside of a provider") // dispatch
]);
But you also need to figure out why the useContext is getting the default value instead of one from a Provider. I'm not sure if these next two issues will fix that or not.
It looks like the Profile component is inside of two different Store providers. One which is outside of everything and one which is inside of the Wrapper component. These will have two separate instances of state. When there are two providers for the same context React uses the inner one. So any actions that you dispatch from Profile won't update the outer version of Store.
You create the Context variable after you use it in your Store component. You should switch the order.

How to access state from another function

Basically I have a state variable in one component which is in a seperate folder
State.js
const [data, setData] = useState([])
folder structure
Components
State > State js
Manipulate > Manipulate js
Basically I want to be able to pass state from the state file in the state folder and be able to access state from the state js in the manipulate js folder.
Is this possible? What would be the best way to do so?
Thanks!
Generally, the useState hook is used to add state to a functional component. This hook does not create a global state that can be accessed by other components (unless you explicitly pass the state item as a property to child components).
If the Manipulate.js component has been imported in State.js, you could pass the state to Manipulate in the below way:
<Manipulate data={data} />
The above would give you access to data in the manipulate component. You would need to ensure that data has been added as a property in the Manipulate.js file like below:
const Manipulate = ({data}) => {
// Your code here
}
If you also needed the state setter in Manipulate.js, you would need to repeat the above steps and pass down the setData function as a property.
The above is fine in some cases; however, if you need global state (i.e state that's available in many different components) you may want to consider using the useContext hook (docs here) or Redux.
It sounds to me that you are trying to create State that is accessible throughout you application. You have a few options, namely a system like Redux, but React ships with a built-in Context system you can utilize to share state throughout your application.
State.js
import React, { useState, createContext } from "react";
export const StateContext = createContext();
export const StateProvider = props => {
const [data, setData] = useState();
return (
<StateContext.Provider value={[data, setData]}>
{props.children}
</StateContext.Provider>
);
};
Manipulate.js
import React, { useContext } from "react";
import { StateProvider, StateContext } from "../State";
const Manipulate = ()=> {
const [data, setData] = useContext(StateContext);
return (
<div>
<StateProvider>
<div>{props.data}</div>
</StateProvider>
</div>
);
}
export default App;

Next.js app doesn't seem to preserve global context provider set up within _app.js

I have my next.js app set up with a global context/provider dealy:
_app.js:
import {useState} from "react"
import GlobalContext from "components/GlobalContext"
function MyApp({ Component, pageProps }) {
const [isLoggedIn, setIsLoggedIn] = useState(null)
let contextValues = {
isLoggedIn, setIsLoggedIn
}
return (
<GlobalContext.Provider value={contextValues}>
<Component {...pageProps} />
</GlobalContext.Provider>
)
}
export default MyApp
components/GlobalContext.js:
const { createContext } = require("react")
const GlobalContext = createContext()
export default GlobalContext
When I call setIsLoggedIn(true) elsewhere in the app... it appears to be set just fine, but then when I navigate to a different page, it seems this global state gets blasted away and isLoggedIn is set back to false.
Is this how it's supposed to work?
How should I manage global state such as this, and also have it persist across page loads?
Thanks.
As I understand it, every time you go to a different page, the function MyApp is run, so a new 'instance' of the component's state is created.
To reuse the state, it sould be stored in a global state. I did that using an external redux store, and calling it from myapp. This store is a global variable and it's reused if it exists. Kind of the 'singleton' pattern.
let store: Store<StoreState, Action>;
const initStore = () => {
if (!store) {
store = createStore(reducer, initialState);
}
return store;
};
export const useStore = () => initStore();
Them from MyApp
import { useStore } from '../store';
import { Provider } from 'react-redux';
export default class MyApp extends App {
render() {
const { Component, pageProps } = this.props;
const store = useStore();
return (
<Provider store={store}>
<Component {...pageProps} />
</Provider>
);
}
}
You can maybe do the same without redux. The key is to keep the state as global variable.
However, this will work only from client side navigation. If you retrieve a page using server side, you'll lose the state, you'll need to use sessions or cookies for that.
You can also check this example from nextjs:
https://github.com/vercel/next.js/tree/canary/examples/with-redux

Resources