how to use functional mobX store in react class components? - reactjs

here is my init store function:
import React, { FC } from 'react';
import { useLocalObservable } from 'mobx-react';
import { TestStore, ITestStore } from './TestStore';
interface IStoreContext {
testStore: ITestStore;
}
export const StoreContext = React.createContext<IStoreContext>({} as IStoreContext);
export const StoreProvider: FC = ({ children }) => {
const testStore = useLocalObservable(TestStore);
const stores = {
testStore,
};
return <StoreContext.Provider value={stores}>{children}</StoreContext.Provider>;
};
export const useRootStore = () => {
const rootStore = React.useContext(StoreContext);
if (!rootStore) {
throw new Error('useStore must be used within a StoreProvider');
}
return rootStore;
};
and this is how i use it:
const { testStore } = useRootStore();
But I can't use the hook inside the class component.
So how get the store inside the class component ?
thanks.

You can create a Higher-Order Component that uses your hook and passes the result to the wrapped class component.
Example
function withRootStore(Component) {
return function WrappedComponent(props) {
const rootStore = useRootStore();
return <Component {...props} rootStore={rootStore} />;
}
}
// ...
class MyComponent extends React.Component {
render() {
const { testStore } = this.props.rootStore;
// ...
}
}
export default withRootStore(MyComponent);

Related

How would one pass a component to a helper?

I want to pass a component to a helper and have that helper return an array of objects, each with a component node...
// helpers.ts
import { LINKS } from '../constants';
// error on the next line: Cannot find name 'Component'. ts(2304)
const createLinks = (component: Component) => {
return LINKS.map((props) => {
return ({
content: <Component {...props} />,
id: props.id
});
});
};
// component.tsx
import { List, SpecialLink } from '../components';
import { createLinks } from '../helpers';
const LinkList = () => {
const links = createLinks(SpecialLink);
return <List items={links}>
}
You should use the ComponentType type of react, so the component argument can be class component or function component.
type ComponentType<P = {}> = ComponentClass<P> | FunctionComponent<P>;
import React from 'react';
import { ComponentType } from 'react';
const LINKS: any[] = [];
const createLinks = (Component: ComponentType) => {
return LINKS.map((props) => {
return {
content: <Component {...props} />,
id: props.id,
};
});
};

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!

useContext returning an empty object in Gatsby application

When using useContext() I'm getting an empty object.
I created a file that contains my Context, Provider, and Consumer
src/utils/notesContext.js
import PropTypes from "prop-types"
import React, { createContext, useState } from "react"
export const Context = createContext({})
export const Provider = props => {
const {
notes: initialNotes,
selectedNote: initialSelectedNotes,
children,
} = props
const [notes, setNotes] = useState([[""]])
const [selectedNote, setSelectedNote] = useState([[""]])
const addNewNote = note => {
console.log(note)
}
const notesContext = {
notes,
setNotes,
selectedNote,
setSelectedNote,
addNewNote,
}
return <Context.Provider value={notesContext}>{children}</Context.Provider>
}
export const { Consumer } = Context
Provider.propTypes = {
notes: PropTypes.array,
selectedNote: PropTypes.object,
}
Provider.defaultProps = {
notes: [],
selectedNote: {},
}
Then within my index file, I have the following
src/pages/index.js
import React, { useContext } from "react"
import { Context } from "../utils/notesContext"
const Index = ({ data, location }) => {
const initNote = data.note
const notesContext = useContext(Context) // notesContext is empty
const { notes, selectedNote, setSelectedNote, addNewNote } = notesContext
addNewNote(initNote)
...
}
Did I set up something incorrectly with my context or provider? Another thing worth mentioning is that this is in a Gatsby application so not sure if this could be causing an underlying issue that I'm not aware of.
Thanks in advance!
Your Index component should be used inside notesContext Provider.
Read more about Context.Provider.
import { Context, Provider } from "./notesContext";
const Index = () => {
const notesContext = useContext(Context);
console.log(notesContext); // now this is not an empty object
return <p>Index</p>;
};
export default function App() {
// notice here, we're wrapping Index inside our Provider
return (
<Provider>
<Index />
</Provider>
);
}

Consuming a context within another context in React

I currently have 2 contexts within my React app and I was trying to call a method from my top-level context within my 2nd context.
Here is how the context are nested:
App.js
function App(props) {
return (
<SessionContextProvider>
<APIContextProvider>
// I have some components here
</APIContextProvider>
</SessionContextProviders>
)
}
is there a way to consume the SessionContext within my APIContextProvider?
import { SessionContext } from 'contexts/session'
export const APIContext = createContext();
export default class APIContextProvider extends Component {
static contextType = SessionContext
randomMethod() {
this.context.logoutUser()
}
render() {
return (
<APIContext.Provider value={{randomMethod: this.randomMethod}}>
{this.props.children}
</APIContext.Provider>
)
}
}
The issue is that when running randomMethod within my APIContext doesn't work because this.context is undefined.
Is this feasible or am I missing something?
I created an example for you, where ApiProvider uses logoutUser from SessionContext and providing randomMethod, which calls the function logoutUser.
import React, { createContext } from "react";
const SessionContext = createContext();
const SessionProvider = props => {
const logoutUser = () => {
alert("Logout user, but fast!");
};
return (
<SessionContext.Provider value={logoutUser}>
{props.children}
</SessionContext.Provider>
);
};
export { SessionContext as default, SessionProvider };
Inner context
import React, { createContext, useContext } from "react";
import SessionContext from "./SessionContext";
const ApiContext = createContext();
const ApiProvider = props => {
const logoutUser = useContext(SessionContext);
const randomMethod = () => {
logoutUser();
};
return (
<ApiContext.Provider value={{ randomMethod: randomMethod }}>
{props.children}
</ApiContext.Provider>
);
};
export { ApiContext as default, ApiProvider };
App.js
export default function App() {
return (
<SessionProvider>
<ApiProvider>
<TestComponent />
</ApiProvider>
</SessionProvider>
);
}
https://codesandbox.io/s/late-bush-959st

How to create a new React component with null return but still access hooks

Instead of putting a react component in the dom is it possible to create a new component in the code, much like when you create a new javascript object?
import * as React from "react";
import Nodetest from "./nodetest";
export default function App() {
const makeNewNode = () => {
const NewNode = new Nodetest();
NewNode.makelog();
};
return <button onClick={makeNewNode}>Make New node</button>;
}
Nodetest has a null return but does not allow me to call the the useContext hook.
import { Component, useContext } from "react";
import { Dispatch, DRAW } from "./global";
class Nodetest extends Component {
test: string;
dispatch: any;
constructor() {
super();
this.test = "hello";
this.dispatch = useContext(Dispatch);
}
makelog = () => {
this.dispatch({ type: DRAW, value: Date.now() });
console.log("new log");
};
render() {
return null;
}
}
export { Nodetest };
UPDATE
I've created a sandbox here https://codesandbox.io/s/eager-wiles-0h2uz but super() gives the error index.d.ts(449, 21): An argument for 'props' was not provided. and clicking the button results in Invalid hook call. Hooks can only be called inside of the body of a function component
Update
If you really want to use class you can inject dispatch in the class constructor.
Not 100% sure if this will work but you can try!
// Nodetest.ts
export default class Nodetest {
test: string;
dispatch: any;
constructor(dispatch) {
this.test = "hello";
this.dispatch = dispatch;
}
makelog = () => {
this.dispatch({ type: DRAW, value: Date.now() });
console.log("new log");
};
}
import * as React from "react";
import { Dispatch, DRAW } from "./global";
import Nodetest from "./Nodetest";
export default function App() {
const dispatch = React.useContext(Dispatch);
const makeNewNode = () => {
const NewNode = new Nodetest(dispatch);
NewNode.makelog();
};
return <button onClick={makeNewNode}>Make New node</button>;
}
Old
You can only use React hooks with functional components. From the example that you gave the best you can do is:
import * as React from "react";
import { Dispatch, DRAW } from "./global";
export default function App() {
const dispatch = React.useContext(Dispatch);
cosnt makelog = () => {
dispatch({ type: DRAW, value: Date.now() });
console.log("new log");
};
const makeNewNode = () => {
makelog();
};
return <button onClick={makeNewNode}>Make New node</button>;
}

Resources