How to set zustand state in a class component - reactjs

I am working on a site that has a piece a global state stored in a file using zustand. I need to be able to set that state in a class component. I am able to set the state in a functional component using hooks but I'm wondering if there is a way to use zustand with class components.
I've created a sandbox for this issue if that's helpful:
https://codesandbox.io/s/crazy-darkness-0ttzd
here I'm setting state in a functional component:
function MyFunction() {
const { setPink } = useStore();
return (
<div>
<button onClick={setPink}>Set State Function</button>
</div>
);
}
my state is stored here:
export const useStore = create((set) => ({
isPink: false,
setPink: () => set((state) => ({ isPink: !state.isPink }))
}));
how can I set state here in a class componet?:
class MyClass extends Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
return (
<div>
<button
onClick={
{
/* setPink */
}
}
>
Set State Class
</button>
</div>
);
}
}

A class component's closest analog to a hook is the higher order component (HOC) pattern. Let's translate the hook useStore into the HOC withStore.
const withStore = BaseComponent => props => {
const store = useStore();
return <BaseComponent {...props} store={store} />;
};
We can access the store as a prop in any class component wrapped in withStore.
class BaseMyClass extends Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
const { setPink } = this.props.store;
return (
<div>
<button onClick={setPink}>
Set State Class
</button>
</div>
);
}
}
const MyClass = withStore(BaseMyClass);

Seems that it uses hooks, so in class you can work with the instance:
import { useStore } from "./store";
class MyClass extends Component {
render() {
return (
<div>
<button
onClick={() => {
useStore.setState({ isPink: true });
}}
>
Set State Class
</button>
</div>
);
}
}

Create a React Context provider that both functional and class-based components can consume. Move the useStore hook/state to the context Provider.
store.js
import { createContext } from "react";
import create from "zustand";
export const ZustandContext = createContext({
isPink: false,
setPink: () => {}
});
export const useStore = create((set) => ({
isPink: false,
setPink: () => set((state) => ({ isPink: !state.isPink }))
}));
export const ZustandProvider = ({ children }) => {
const { isPink, setPink } = useStore();
return (
<ZustandContext.Provider
value={{
isPink,
setPink
}}
>
{children}
</ZustandContext.Provider>
);
};
index.js
Wrap your application with the ZustandProvider component.
...
import { ZustandProvider } from "./store";
import App from "./App";
const rootElement = document.getElementById("root");
ReactDOM.render(
<StrictMode>
<ZustandProvider>
<App />
</ZustandProvider>
</StrictMode>,
rootElement
);
Consume the ZustandContext context in both components
MyFunction.js
import React, { useContext } from "react";
import { ZustandContext } from './store';
function MyFunction() {
const { setPink } = useContext(ZustandContext);
return (
<div>
<button onClick={setPink}>Set State Function</button>
</div>
);
}
MyClass.js
import React, { Component } from "react";
import { ZustandContext } from './store';
class MyClass extends Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
return (
<div>
<button
onClick={this.context.setPink}
>
Set State Class
</button>
</div>
);
}
}
MyClass.contextType = ZustandContext;
Swap in the new ZustandContext in App instead of using the useStore hook directly.
import { useContext} from 'react';
import "./styles.css";
import MyClass from "./MyClass";
import MyFunction from "./MyFunction";
import { ZustandContext } from './store';
export default function App() {
const { isPink } = useContext(ZustandContext);
return (
<div
className="App"
style={{
backgroundColor: isPink ? "pink" : "teal"
}}
>
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<MyClass />
<MyFunction />
</div>
);
}
If you aren't able to set any specific context on the MyClass component you can use the ZustandContext.Consumer to provide the setPink callback as a prop.
<ZustandContext.Consumer>
{({ setPink }) => <MyClass setPink={setPink} />}
</ZustandContext.Consumer>
MyClass
<button onClick={this.props.setPink}>Set State Class</button>

This worked out pretty well for me.
:
import React, { Component } from "react";
import { useStore } from "./store";
class MyClass extends Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
return (
<div>
<button
onClick={
useStore.getState().setPink() // <-- Changed code
}
>
Set State Class
</button>
</div>
);
}
}
export default MyClass;

I like to create a high order component similar to redux connect:
function connectZustand(useStore, selector) {
return (Component) =>
React.forwardRef((props, ref) => <Component ref={ref} {...props} {...useStore(selector, shallow)} />);
}
eg:
import React, { Component } from 'react';
import create from 'zustand';
import shallow from 'zustand/shallow';
function connectZustand(useStore, selector) {
return (Component) =>
React.forwardRef((props, ref) => <Component ref={ref} {...props} {...useStore(selector, shallow)} />);
}
const useStore = create((set) => ({
isPink: false,
setPink: () => set((state) => ({ isPink: !state.isPink })),
}));
class MyClass extends Component {
render() {
const { setPink } = this.props;
return (
<div>
<button onClick={() => setPink()}>Set State Class</button>
</div>
);
}
}
const MyClassWithZustand = connectZustand(useStore, (state) => ({ setPink: state.setPink }))(MyClass);
export default function Test() {
const isPink = useStore((state) => state.isPink);
return (
<>
<MyClassWithZustand />
{isPink ? 'Is Pink' : 'Is Not Pink'}
</>
);
}

Related

How to update context variable in ReactJS?

import React, { Component } from 'react'
export default class Class extends Component {
constructor(){
super();
this.state={
data:"NULL"
};
}
setData(value) {
this.setState({ data:value})
}
render() {
return (
<div>
<div>{this.state.data}</div>
<button onClick={()=>this.setData("Say Hi!")}>
Say Hi!
</button>
<button onClick={()=>this.setData("Say Hello!")}>
Say Hello!
</button>
</div>
)
}
}
Now i want to maintain this state using Context API(global state).
MyContext Class:
import React, { useState } from 'react';
const ClassContext = React.ClassContext();
const ClassProvider = (props) => {
const [data, setData] = useState("NULL");
return (
<ClassContext.Provider
value={{
data,
setData,
}}>
{props.children}
</ClassContext.Provider>
)
}
export { ClassContext, ClassProvider };
We can update state variable inside function in the first code snippet. But how to do the same update with context api?
Here is the working example
import React, { useState, createContext, Component } from "react";
const ClassContext = createContext();
export default function App() {
return (
<ClassProvider>
<Class />
</ClassProvider>
);
}
function ClassProvider(props) {
const [data, setData] = useState("NULL");
return (
<ClassContext.Provider value={{ data, setData }}>
{props.children}
</ClassContext.Provider>
);
}
class Class extends Component {
render() {
const { data, setData } = this.context;
return (
<div>
<div>{data}</div>
<button onClick={() => setData("Say Hi!")}>Say Hi!</button>
<button onClick={() => setData("Say Hello!")}>Say Hello!</button>
</div>
);
}
}
Class.contextType = ClassContext;
CSB example - I might delete CSB example at some point.
Alternatively you can also consume data and setData in class as
class Class extends Component {
render() {
return (
<ClassContext.Consumer>
{({ data, setData }) => (
<div>
<div>{data}</div>
<button onClick={() => setData("Say Hi!")}>Say Hi!</button>
<button onClick={() => setData("Say Hello!")}>Say Hello!</button>
</div>
)}
</ClassContext.Consumer>
);
}
}
In the above example you dont need to assign contextType property to class

Why component with Context Provider doesn't re-render

I was looking for the answer why react component with Context Provider doesn't re-render but i couldn't find answer proper for me to understand why.
Moreover i want to mention Im using GatsbyJS.
Here's App.context.js:
const defaultValue = {
menu: false,
handleMenu: () => { },
}
const AppContext = createContext(defaultValue);
export default AppContext;
export { defaultValue };
Next, down below there's Provider element App.provider.js:
import AppContext, { defaultValue } from './App.context';
class AppProvider extends Component {
constructor(props) {
super(props);
this.state = defaultValue
}
handleMenu = () => {
if (this.state.menu) {
this.setState({
menu: false
})
} else {
this.setState({
menu: true
})
}
}
render() {
return (
<AppContext.Provider value={{
menu: this.state.menu,
handleMenu: this.handleMenu,
}}>
{this.props.children}
</AppContext.Provider>
);
}
}
export default AppProvider;
Then, I'm using this provider at the beginning of elements tree:
//Components
import Header from '../components/header';
import Footer from '../components/footer';
import MainWrap from '../components/mainWrap';
//Context
import AppProvider from '../context/App.provider';
const Layout = ({ children }) => {
return (
<AppProvider>
<MainWrap>
<Header />
{children}
<Footer />
</MainWrap>
</AppProvider>
);
}
export default Layout;
Here's MainWrap component:
//Styles
import wrapStyles from '../styles/wrapper.module.scss';
//Context
import AppContext from '../context/App.context';
const MainWrap = ({children}) => {
const {menu} = useContext(AppContext);
return (
<div className={menu?wrapStyles.wrap:wrapStyles.wrapActive}>{children}</div>
);
}
export default MainWrap;
When context value change, child components like MainPage re-render properly, but why component with Provider does not, so i can't instead of using next wrap component (MainPage) just put a div in component with Provider:
//Components
import Header from '../components/header';
import Footer from '../components/footer';
//Styles
import wrapStyles from '../styles/wrapper.module.scss';
//Context
import AppProvider from '../context/App.provider';
import AppContext from '../context/App.context';
const Layout = ({ children }) => {
const {menu} = useContext(AppContext);
return (
<AppProvider>
<div className={menu?wrapStyles.wrap:wrapStyles.wrapActive}>
<Header />
{children}
<Footer />
</div>
</AppProvider>
);
}
export default Layout;
I hope it will be understandable.

How to pass the data from child to parent? when parent is class based and child is functional based

I want to pass the data which is anything like, array, strings, numbers into the App(Parent) Component) from the Child1(Child) components.
There's a parent who is class-based and the child is functional based.
Parent:
import React, { Component } from "react";
import "./App.css";
import Child1 from "./Child1/Child1";
class App extends Component {
state = { message: "" };
callbackFunction = (event) => {
this.setState({
message: event.target.value
});
}
render() {
return (
<div className="App">
<Child1 />
</div>
);
}
}
export default App;
Child
import React from 'react'
const Child1 = (props) => {
return (
<div>
</div>
);
}
export default Child1;
If you want to send some data to the parent component upon clicking a button that's in the child component, you can do it this way:
import React from 'react'
const Child1 = React.memo((props) => {
return (
<div>
<button onClick={() => {props.clicked('some data')}} type="button">Click me to send data<button>
</div>
);
})
export default Child1;
now you can assign a function to the clicked prop in your parent component and that gets called when the button in the child component is clicked:
class App extends Component {
state = { message: "" };
callbackFunction = (event) => {
this.setState({
message: event.target.value
});
}
clicked = (data) => { // this func. will be executed
console.log(data);
}
render() {
return (
<div className="App">
<Child1 clicked={this.clicked}/>
</div>
);
}
}
I have founded another solution for you using combine Functional-Class-functional component pass data. here the live code for https://codesandbox.io Click and check that.
Also same code added bellow here:
app component
import React, { useState } from "react";
import "./styles.css";
import Child from "./child";
export default function App() {
const [name, setName] = useState("Orignal Button");
const [count, setCount] = useState(0);
const handleClick = _name => {
setName(_name);
setCount(count + 1);
};
const resetClick = e => {
setName("Orignal Button");
setCount(0);
};
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<Child
name={name + " - " + count}
handleClick={handleClick}
resetClick={resetClick}
/>
</div>
);
}
child1 component
import React from "react";
import Child2 from "./child2";
class Child extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<>
<h2>This is child</h2>
<button onClick={e => this.props.handleClick("New Name changed")}>
{this.props.name}
</button>
<Child2 handleChilc2Click={this.props.resetClick} />
</>
);
}
}
export default Child;
Another component Child2
import React from "react";
const Child2 = props => {
return (
<>
<h2>Child 2</h2>
<button onClick={e => props.handleChilc2Click(e)}>
Click me for reset Parent default
</button>
</>
);
};
export default Child2;
You can some help from it.
Thanks
You can pass a function to Child (called Counter in the example) that will change Parent state. Since you pass the same reference for this function every time (this.increment) the only Children that will render are those that changed.
const Counter = React.memo(
//use React.memo to create pure component
function Counter({ counter, increment }) {
console.log('rendering:', counter.id)
// can you gues why prop={new reference} is not a problem here
// this won't re render if props didn't
// change because it's a pure component
return (
<div>
<div>counter is {counter.count}</div>
<button onClick={() => increment(counter.id)}>
+
</button>
</div>
)
}
)
class Parent extends React.PureComponent {
state = {
counters: [
{ id: 1, count: 0 },
{ id: 2, count: 0 }
]
}
increment = _id =>
this.setState({
counters: this.state.counters.map(val =>
_id === val.id
? { ...val, count: val.count + 1 }
: val
)
})
render() {
return (
<div>
{this.state.counters.map(counter => (
<Counter
counter={counter}
increment={this.increment}
key={counter.id}
/>
))}
</div>
)
}
}
//render app
ReactDOM.render(<Parent />, document.getElementById('root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

jest + enzyme access to instance of a connected component

I have an issue using jest and enzymes for my unit testing on react. I have a component connected to redux and i want to test a method in my component that update the state. heres's my component :
import React, {Component} from 'react';
import Form from 'react-jsonschema-form';
import pick from 'lodash/pick';
import {bindActionCreators} from 'redux';
import fields from './fields';
import widgets from './widgets';
import {initContainerActions} from '../../utils/actionHelper';
import {actions as modelActions} from './actions';
import {connect} from '../../utils/connect';
import reducerRegistry from '../ReducerRegistry';
import reducer from './reducer';
type StateProperties = {
roleModelSchema: Object,
roleUiSchema: Object
};
export type FormContainerProps = {
model: Object,
uiSchema: Object,
userModelLoad: Function,
isLoading: boolean,
idContainer: string
};
export class FormContainer extends Component<FormContainerProps, StateProperties> {
fields = {...fields};
widgets = {...widgets};
constructor(props) {
super(props);
const {idContainer} = props;
reducerRegistry.register(idContainer, reducer(idContainer));
this.state = {
roleModelSchema: {},
roleUiSchema: {}
};
}
componentDidMount = async () => {
const {userModelLoad} = this.props;
await userModelLoad();
this.renderRoleForm();
};
renderRoleForm = () => {
const {model, uiSchema} = this.props;
const modelProperties = Object.keys(model.properties);
const roleUiSchema = pick(uiSchema, modelProperties);
this.setState({
roleUiSchema,
roleModelSchema: model
});
};
render = () => {
const {roleModelSchema, roleUiSchema} = this.state;
const {isLoading} = this.props;
if (isLoading) {
return <div>...Loading</div>;
}
return (
<div className="container">
<div className="columns">
<div className="column col-12">
<Form schema={roleModelSchema} uiSchema={roleUiSchema} liveValidate fields={this.fields} widgets={this.widgets}>
<div>
<button type="submit" className="btn btn-primary">
Submit
</button>
<button type="button">Cancel</button>
</div>
</Form>
</div>
</div>
</div>
);
};
}
const mapDispatchToProps = (dispatch, props) => {
const initializedActions = initContainerActions(modelActions, props.idContainer);
return bindActionCreators(initializedActions, dispatch);
};
export default connect(
(state, props) => state[props.idContainer] || {},
mapDispatchToProps
)(FormContainer);
And the test :
const mockStore = configureMockStore([]);
const context = React.createContext();
describe('Check if renderRoleForm update state', () => {
it('renders without crashing', () => {
// Initialize mockstore with empty state
const initialState = {roleUiSchema: {}};
const store = mockStore(initialState);
const wrapper = shallow(
<Provider store={store} context={context}>
<FormContainer model={model} uiSchema={uiSchema} context={context}
/>
</Provider>
);
const instance = wrapper.instance();
instance.renderRoleForm();
expect(wrapper.state().roleUiSchema).not.toBeEmpty();
});
});
The error i have : TypeError: instance.renderRoleForm is not a function
I tried with mount, dive with shallow and same problem.
If you have any idea :)
Thanks
const wrapper = mount(
<Provider store={store} context={context}>
<FormContainer model={model} uiSchema={uiSchema} context={context} />
</Provider> );
//
wrapper.instance().renderRoleForm();

React Context and Next JS

I'm trying to add simple React Context to my app. I create Context in "./components/DataProvider.js" that looks like this:
import React, { Component } from 'react'
const DataContext = React.createContext()
class DataProvider extends Component {
state = {
isAddButtonClicked: false
}
changeAddButtonState = () => {
if( this.state.isAddButtonClicked ) {
this.setState({
isAddButtonClicked: false
})
} else {
this.setState({
isAddButtonClicked: true
})
}
}
render() {
return(
<DataContext.Provider
value={{
isAddButtonClicked: this.state.isAddButtonClicked,
changeAddButtonState: () => {
if( this.state.isAddButtonClicked ) {
this.setState({
isAddButtonClicked: false
})
} else {
this.setState({
isAddButtonClicked: true
})
}
}
}}
>
{this.props.children}
</DataContext.Provider>
)
}
}
const DataConsumer = DataContext.Consumer
export default DataProvider
export { DataConsumer }
Which then I added to "./pages/_app.js"
import App, { Container } from 'next/app'
import DataProvider from '../components/DataProvider'
class MyApp extends App {
render () {
const { Component, pageProps } = this.props
return (
<Container>
<DataProvider>
<Component {...pageProps} />
</DataProvider>
</Container>
)
}
}
export default MyApp
And consume it in "./components/AddPostButton.js".
import React, {Component} from 'react'
import { DataConsumer } from './DataProvider'
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome'
import { faPlus } from '#fortawesome/free-solid-svg-icons'
class AddPostButton extends Component {
render() {
return (
<div>
<DataConsumer>
{({ changeAddButtonState }) => (
<a onClick={changeAddButtonState}>
<FontAwesomeIcon icon={faPlus} color='#fff' />
</a>
)}
</DataConsumer>
</div>
)
}
}
export default AddPostButton
But I get this error "Cannot read property 'changeAddButtonState' of undefined". I'm using React 16.7 and NextJS 7.0.2. Don't know what is wrong.
The second question is should I use one Context for everything or just use them as Model in MVC pattern?
I fixed it by moving changeAddButtonState to Context Component state so my DataProvider.js now looks like this
import React, { Component } from 'react'
const DataContext = React.createContext()
class DataProvider extends Component {
state = {
isAddButtonClicked: false,
changeAddButtonState: () => {
if (this.state.isAddButtonClicked) {
this.setState({
isAddButtonClicked: false
})
} else {
this.setState({
isAddButtonClicked: true
})
}
}
}
render() {
return(
<DataContext.Provider
value={this.state}
>
{this.props.children}
</DataContext.Provider>
)
}
}
const DataConsumer = DataContext.Consumer
export default DataProvider
export { DataConsumer }
And then in AddButton component I changed code to look like this
import React, {Component} from 'react'
import { DataConsumer } from './DataProvider'
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome'
import { faPlus } from '#fortawesome/free-solid-svg-icons'
class AddPostButton extends Component {
constructor(props) {
super(props)
}
render() {
return (
<div>
<DataConsumer>
{(context) => (
<a onClick={context.changeAddButtonState}>
<FontAwesomeIcon icon={faPlus} color='#fff' />
</a>
)}
</DataConsumer>
</div>
)
}
}
export default AddPostButton

Resources