First, I have my state variable
export const state: State = {
assetPricesInUsd: {
BTC: '20000',
},
supportedAssets: [],
appLoading: true
}
and of course my overmind config as follows:
export const config = merge(
{
state,
actions,
effects,
},
namespaced({
swap,
send,
receive,
wallet,
}),
)
And inside my _app.tsx, since I am using NextJS, I create my app with the Overmind provider as so:
import { config } from '../lib/overmind'
const overmind = createOvermind(config)
function MyApp({ Component, pageProps }: AppProps) {
return (
<OvermindProvider value={overmind}>
<div data-theme='forest'>
<Navbar />
<Component {...pageProps} />
<Toaster position='top-center' />
</div>
</OvermindProvider>
)
}
export default MyApp
And as a super simple example, I created an 'appLoading' property of my state (as seen above) which initializes to true and is set to false at the end of onInitializeOvermind
export const onInitializeOvermind = async ({
effects,
actions,
state,
}: Context) => {
await actions.loadSupportedAssets()
await actions.loadAssetPrices(state.supportedAssets)
console.log('finished initializing')
state.appLoading = false
}
And in my index.tsx page route, I would like the page to respond to the state.appLoading changing like so:
import { useAppState } from 'lib/overmind'
const Home: NextPage = () => {
const { appLoading } = useAppState()
if (appLoading) {
console.log('app is loading')
return <div>loading...</div>
}
return (
<>
<Head>
<title>Create Next App</title>
<meta name='description' content='Generated by create next app' />
<link rel='icon' href='/favicon.ico' />
</Head>
<main className='min-h-screen bg-base grid grid-cols-8 font-Josefin'>
<Sidebar />
<Trade />
</main>
</>
)
}
export default Home
But in my app, <div>Loading...</div> is rendered on initial load, but it is not updated when I set state.appLoading to false.
In the devtools, appLoading is set to true, and the 'finished initializing' is printed to the console, but the page does not rerender.
Am I doing something wrong with accessing the state variable in my Home component, because it seems that the state is not linked to my component, and overmind doesn't know to re-render it?
Edit: There isn’t an issue with creating action/state hooks. My app state changes with actions like state.appLoading, and if I interact with a component THEN its state updates, but if I log in through the Navbar (updating the isConnected state) then the Sidebar component, which uses the isComponent property from the useState hook, doesn’t rerender until I change something about its own internal state.
Edit 2: I made a sample app which highlights the issue I'm facing hosted here: https://github.com/MaxSpiro/example-overmind. This example confirms that the state is being updated, but what I found out is that the two components are not being rendered at first. Only when you change the internal state of the components (with the second button) do they begin to react to the global state of overmind changing.
Edit 3: https://github.com/cerebral/overmind/issues/553
Related
Background to the problem
HOCs
When dealing with Next.Js pages a common practice is to use HOC (Higher-Order Component) to avoid retyping the base of a page.
For example, an authentication HOC can be used to check if a user is authenticated or not. Depending on the outcome the user can either access the page or be redirected to a sign-in page.
Layouts
Another practice that is commonly used by Next.Js programmers is Persistent Layouts. The Persistent layout is an "area" on the page that will not re-render when the user is navigating through pages. This is great for UX (User Experience), for example, the scroll-position of a menu remains on page switch.
Some good links
NextJs.org - Persistent Layout Documentation
CheatCode.co - How to Handle Authenticated Routes (The HOC used below)
The problem
When combining these two practices a problem with persistence occurs. (Read comments inside AuthenticationRoute.js
This is a very simple index.js
import authenticatedRoute from '#components/auth/AuthenticatedRoute';
const App = () => {
return (
<section>
<h1>Logged In</h1>
<h1>App</h1>
</section>
);
};
export default authenticatedRoute(App);
The layout Layout.js
import Link from 'next/link';
import Navbar from '#components/layouts/Navbar';
import SideMenu from '#components/layouts/SideMenu';
function Layout({ children }) {
const baseUrl = '/app';
return (
<section>
<Navbar>
<li>
<Link href={`${baseUrl}/`}>Home</Link>
</li>
<li>
<Link href={`${baseUrl}/test1`}>Test 1</Link>
</li>
<li>
<Link href={`${baseUrl}/test2`}>Test 2</Link>
</li>
</Navbar>
<main>{children}</main>
</section>;
);
}
export default Layout;
And lastly the HOC AuthenticationRoute.js
import { Component as component } from 'react';
import Router from 'next/router';
import Layout from '#components/layouts/app/Layout';
const authenticatedRoute = (Component = null, options = {}) => {
class AuthenticatedRoute extends component {
state = {
loading: true,
};
componentDidMount() {
const isSignedIn = true;
if (isSignedIn) {
this.setState({ loading: false });
} else {
Router.push(options.pathAfterFailure || '/sign_in');
}
}
render() {
const { loading } = this.state;
if (loading) {
return <div />;
}
// This will return the page without the layout
return <Component {...this.props} />;
// Removing the line above and using this instead
// the page now renders with the layout. BUT...
// The layout is not persistent, it will re-render
// everytime a user navigate.
const getLayout = (page) => <Layout>{page}</Layout>;
return getLayout(<Component {...this.props} />);
}
}
return AuthenticatedRoute;
};
export default authenticatedRoute;
Without HOC
Inside index.js this is working when not calling authenticatedRoute:
App.getLayout = getLayout;
export default App;
So my guess is that the authenticatedRoute should return something else then return <Component {...this.props} />;
I know this is horrible convention, but I'm trying to quickly conditionally render screens in my React Native app with global variables (so no redux):
App.js:
if (global.clickStatus !== 'clicked') {
return <Screen1 />;
}
return <Screen2 />;
The app begins on Screen1, where there is a button that makes global.clickStatus = 'clicked'. When this is clicked, I want Screen2 to render. The problem is, the global.clickStatus doesn't seem to update on my App.js (even though global.clickStatus is changed, it still renders Screen1.
How can I get it to update?
I believe in <App /> component because it is a function component you can introduce a state if your button is clicked. Then with clicked state you can manipulate which component to show.
Similarly like the following - obviously this is a simplified example:
const App = () => {
const [clicked, setClicked] = useState(false);
return <>
<div onClick={() => setClicked(true)}>Click me</div>
{ clicked ? <Screen2 /> : <Screen1 /> }
</>
}
Suggested read is Using the State Hook.
The app begins on Screen1, where there is a button that makes global.clickStatus = 'clicked'
When you click the button, you did not set any state for App.js component => no re-render action is made.
I just assume the button is in Screen 1. Try code below:
import React from "react";
import "./styles.css";
export default function App() {
// Create a state
const [renderIndex, setRenderIndex] = useState(new Date().getTime())
if (global.clickStatus !== 'clicked') {
// Assume you have a button in Screen1
// Pass a callback function from this component to Screen1
// When button in Screen1 is clicked, call this callback function to update renderIndex => App component will re-render
return <Screen1 callBack={() => setRenderIndex(new Date().getTime())}/>;
}
return <Screen2 />;
}
This is my first React Context implementation. I am using Gatsby and in my layout.js I added Context (with objects and handler function) that successfully gets passed to Consumer:
import AppContext from "./AppContext"
const Layout = ({ children }) => {
let doAuthenticate = () => {
authState = {
isAuth: authState.isAuth === true ? false : true,
}
}
let authState = {
isAuth: false,
doAuthenticate: doAuthenticate
}
return (
<>
<AppContext.Provider value={authState}>
<main>{children}</main>
</AppContext.Provider>
</>
)
I successfully execute function in Consumer:
<AppContext.Consumer>
{ value =>
<button onClick={value.doAuthenticate}Sign in</button>
}
</AppContext.Consumer>
I also see the value in doAuthenticate successfully gets updated.
However, another Consumer that is listening to Provider does not update the value. Why?
When you use Gatsby, each instance of the Page will we wrapped with the Layout component and hence you will see that instead of creating one Context that is shared between pages, you end up creating multiple contexts.
Now multiple contexts cannot communicate with each other
The solution here is to make use of wrapRootElement api in gatsby-ssr.js and gatsby-browser.js to wrap all your pages with a single layout component
import React from "react";
import Layout from "path/to/Layout";
const wrapRootElement = ({ element }) => {
return (
<Layout>
{element}
</Layout>
)
}
export { wrapRootElement };
Trying to render state from Context API, but in console it shows as undefined and doesn't render anything.
here is Context file
import React, { useReducer, createContext } from "react"
export const GlobalStateContext = createContext()
export const GlobalDispatchContext = createContext()
const initialState = {
isLoggedIn: "logged out",
}
function reducer(state, action) {
switch (action.type) {
case "TOGGLE_LOGIN":
{
return {
...state,
isLoggedIn: state.isLoggedIn === false ? true : false,
}
}
break
default:
throw new Error("bad action")
}
}
const GlobalContextProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState)
return (
<GlobalStateContext.Provider value={state}>
{children}
</GlobalStateContext.Provider>
)
}
export default GlobalContextProvider
and here is where value should be rendered
import React, { useContext } from "react"
import {
GlobalStateContext,
GlobalDispatchContext,
} from "../context/GlobalContextProvider"
const Login = () => {
const state = useContext(GlobalStateContext)
console.log(state)
return (
<>
<GlobalStateContext.Consumer>
{value => <p>{value}</p>}
</GlobalStateContext.Consumer>
</>
)
}
export default Login
I tried before the same thing with class component but it didn't solve the problem. When I console log context it looks like object with undefined values.
Any ideas?
The Context API In General
From the comments, it seems the potential problem is that you're not rendering <Login /> as a child of <GlobalContextProvider />. When you're using a context consumer, either as a hook or as a function, there needs to be a matching provider somewhere in the component tree as its parent.
For example, these would not work:
<div>
<h1>Please log in!</h1>
<Login />
</div>
<React.Fragment>
<GlobalContextProvider />
<Login />
</React.Fragment>
because in both of those, the Login component is either a sibling of the context provider, or the provider is missing entirely.
This, however, would work:
<React.Fragment>
<GlobalContextProvider>
<Login />
</GlobalContextProvider>
</React.Fragment>
because the Login component is a child of the GlobalContextProvider.
Related To Gatsby
This concept is true regardless of what library or framework you're using to make your app. In Gatsby specifically there's a little bit of work you have to do to get this to work at a page level, but it's possible.
Let's say you have a Layout.jsx file defined, and the following page:
const Index = () => (
<Layout>
<h1>{something that uses context}</h1>
</Layout>
)
You have 2 options:
The easier option is to extract that h1 into its own component file. Then you can put the GlobalContextProvider in the Layout and put the context consumer in the new component. This would work because the h1 is being rendered as a child of the layout.
Is to do some shuffling.
You might be inclined to put the Provider in the layout and try to consume it in the page. You might think this would work because the h1 is still being rendered as a child of the Layout, right? That is correct, but the context is not being consumed by the h1. The context is being rendered by the h1 and consumed by Index, which is the parent of <Layout>. Using it at a page level is possible, but what you would have to do is make another component (IndexContent or something similar), consume your context in there, and render that as a child of layout. So as an example (with imports left out for brevity):
const Layout = ({children}) => (
<GlobalContextProvider>
{children}
</GlobalContextProvider>
);
const IndexContent = () => {
const {text} = useContext(GlobalStateContext);
return <h1>{text}</h1>;
}
const Index = () => (
<Layout>
<IndexContent />
</Layout>
);
I have the next code:
import React from 'react'
import Loadable from 'react-loadable'
import { Route } from 'react-router-dom'
class App extends React.Component {
state = {
kappa: false
}
componentDidMount() {
setTimeout(() => {
setState({ kappa: true })
}, 1000)
}
render() {
return (
<div className="app">
<Route exact path="/:locale/" component={Loadable({
loader: () => import('../views/IndexPage/index.jsx'),
loading: () => "loading"
})} />
<Route exact path="/:locale/registration" component={Loadable({
loader: () => import('../views/Registration/index.jsx'),
loading: () => "loading"
})} />
{this.state.kappa && <p>Hey, Kappa-Kappa, hey, Kappa-Kappa, hey!</p>}
</div>
)
}
}
When state updates (kappa becomes true and p appears), component on active route (no matter what is it - IndexPage or Registration) remounts. If I import component manually in App and pass it to the Route without code-splitting, components on routes doesn't remount (that's so obvious).
I also tried webpack's dynamic import, like this:
<Route path="/some-path" component={props => <AsyncView {...props} component="ComponentFolderName" />
where import(`/path/to/${this.props.component}/index.jsx`) runs in componentDidMount and upfills AsyncView's state, and it behaves simillar to Loadable situation.
I suppose, the problem is that component for Route is an anonymous function
The question is: how to avoid remount of route components?
Well, this behaviour is normal and documented at React Router 4 docs:
When you use component (instead of render or children, below) the router uses React.createElement to create a new React element from the given component. That means if you provide an inline function to the component prop, you would create a new component every render. This results in the existing component unmounting and the new component mounting instead of just updating the existing component. When using an inline function for inline rendering, use the render or the children prop (below).
render works fine both with React Loader and webpack's code splitting.