React composition: expose higher-order methods - reactjs

Since I've seen React team have discouraged developers to use inheritance, I'm trying to use composition instead.
export class Page extends React.PureComponent {
shouldComponentUpdate(nextProps, nextState) {
return !isEqual(nextProps, this.props) || !isEqual(nextState, this.state);
}
render() {
return this.props.children;
}
}
const mapStateToProps = (state, ownProps) => {
return {}
};
const mapDispatchToProps = (dispatch) => {
return {
onNextClick: () => dispatch(customAction())
}
};
export default connect(mapStateToProps, mapDispatchToProps)(Page);
export class FirstPage extends React.PureComponent {
render() {
return (
<Page>
<div>
<h1>Title</h1>
<Button onClick={this.props.onNextClick}>Next</Button>
</div>
</Page>
);
}
}
const mapStateToProps = (state, ownProps) => {
return {}
};
const mapDispatchToProps = (dispatch) => {
return {
onNextClick: () => dispatch(customAction())
}
};
export default connect(mapStateToProps, mapDispatchToProps)(FirstPage);
In this case, several components will use Page as container and will have a next button. I'd like to expose a generic handler for the next button click in the Page component to avoid repeating the dispatch(customAction()).
I would easily achieve this with inheritance but I'm stuck with the composition pattern.
Any ideas?

I suggest you to provide additional prop for Page children:
export class Page extends React.PureComponent {
shouldComponentUpdate(nextProps, nextState) {
return !isEqual(nextProps, this.props) || !isEqual(nextState, this.state);
}
render() {
const {onNextClick} = this.props;
return React.cloneElement(this.props.children, {onNextClick});
}
}
const mapDispatchToProps = (dispatch) => {
return {
onNextClick: () => dispatch(customAction())
}
};
export default connect(null, mapDispatchToProps)(Page);
export class FirstPage extends React.PureComponent {
render() {
return (
<div>
<h1>Title</h1>
<Button onClick={this.props.onNextClick}>Next</Button>
</div>
);
}
}
export class App extends React.PureComponent {
render() {
return (
<Page>
<FirstPage />
</Page>
);
}
}

You can use ref to refer to the Page element.
class Page extends React.Component {
nextHandler(){
console.log("I am in the parent.");
}
render(){
return this.props.children;
}
}
class FirstPage extends React.Component {
render(){
return (
<Page ref={ page => this.page = page }>
<div>
<h1>Title</h1>
<button onClick={ () => this.page.nextHandler() }>Next</button>
</div>
</Page>
)
}
}
ReactDOM.render(<FirstPage />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>

Related

How to set zustand state in a class component

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'}
</>
);
}

How to update a child component without re-rendering without Redux

How do I update an adjacent (or child) component after a scroll event without re-rendering the parent component?
Adjacent scenario
<div>
<Scrollable onScroll={ ... } />
<DisplayPosition scrollEvent={ ... } />
</div>
Child scenario
return (
<div onScroll={ ... }>
<span> Don’t re-render me! </span>
<DisplayPosition scrollEvent={ ... } />
</div>
)
I am reluctant to reach for Redux for this problem as I would like to be able to solve it in a lightweight fashion
Without Redux
ParentComponent
export default class ParentComponent extends Component {
render() {
let parentCallback = () => {};
const onScroll = event => parentCallback(event);
const didScroll = callback => parentCallback = callback;
return (<div onScroll={onScroll}>
<ChildrenComponent whenParent={didScroll} />
...
ChildrenComponent
It will setup the callback to be executed by the parent
componentDidMount() {
this.props.whenParent(event => this.setState({ event }));
}
By updating the state your ChildrenComponent will re-render. The ParentComponent will not re-render as nothing on its state or properties changed.
You'll have access to your event on this.state.event. Check the snippet for an example with click event.
class ParentComponent extends React.Component {
render() {
console.log('hello ParentComponent');
let parentCallback = () => {};
const onClick = () => parentCallback(1);
const didClick = callback => parentCallback = callback;
return (
<div>
<button onClick={onClick}>Click To Add</button>
<ChildrenComponent whenParent={didClick} />
</div>
);
}
}
class ChildrenComponent extends React.Component {
state = { sum: 0 };
componentDidMount() {
const { whenParent } = this.props;
whenParent((adds) => {
const { sum } = this.state;
this.setState({ sum: sum + adds });
});
}
render() {
console.log('hello ChildrenComponent');
const { sum } = this.state;
return <div>{sum}</div>;
}
}
ReactDOM.render(
<ParentComponent />,
document.getElementById('container')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="container"></div>

Re-render a component on repeat click on the same tab in another component reactjs

I know the title might be confusing, as well as might sound as a repeat, please read the whole description before marking it as repeat, I am new to react and need some help.
I am building a dashboard. I have a navigation bar div which has multiple tabs and a content div which has the corresponding content. Once a tab is clicked i render its corresponding content. Within any tab the user can do various things/changes. Lets say i have a tab ABC which when clicked produces an initial view, when i click this tab again when it is already clicked i need to re-render the ABC tabs content.
Essentially what i want to do is when after clicking test and test-demo once, user clicks test again the text 'test-demo' should disappear.
import React, { Component } from 'react';
const Button = (props) => {
return (
<button onClick={() => props.onClick(props.buttonName.trim())}>{props.buttonName}</button>
);
};
class Test extends Component {
static initialState = () => ({
appContent:null,
});
state = Test.initialState();
switchTab = (buttonKey) => {
this.setState(prevState => ({
appContent:<a>{buttonKey}</a>
}));
}
render() {
let appContent = null;
switch(this.props.navigationTab) {
case "test":
appContent = <Button onClick={this.switchTab} buttonName='test-demo' />
break;
default:
appContent = null
break;
};
return (
<div>
{appContent}
{this.state.appContent}
</div>
);
}
}
class AppContent extends Component {
render() {
return (
<div>
<Test navigationTab={this.props.navigationTab}/>
</div>
);
}
}
class App extends Component {
static initialState = () => ({
navigationTab:null,
});
state = App.initialState();
switchTab = (buttonKey) => {
this.setState(prevState => ({
navigationTab:buttonKey,
}));
}
render() {
return (
<div>
<div>
<Button onClick={this.switchTab} buttonName='test'/>
</div>
<AppContent navigationTab={this.state.navigationTab} />
</div>
);
}
}
export default App;
https://stackblitz.com/edit/react-fs8u7o?embed=1&file=index.js
import React, { Component } from 'react';
import { render } from 'react-dom';
import Hello from './Hello';
import './style.css';
const Button = (props) => {
return (
<button onClick={() => props.onClick(props.buttonName.trim())}>{props.buttonName}</button>
);
};
class Test extends Component {
constructor(props) {
super(props);
this.state = {
appContent: null,
hideTestDemo:false,
};
}
componentWillReceiveProps(nextProps){
this.setState(prevState => ({
hideTestDemo:nextProps.hideTestDemo,
}));
}
switchTab = (buttonKey) => {
this.setState(prevState => ({
appContent: <a>{buttonKey}</a>,
hideTestDemo:false,
}));
}
render() {
let appContent = null;
switch (this.props.navigationTab) {
case "test":
appContent = <Button onClick={this.switchTab} buttonName='test-demo' />
break;
default:
appContent = null
break;
};
return (
<div>
{appContent}
{(!this.state.hideTestDemo ) ? this.state.appContent:null}
</div>
);
}
}
class AppContent extends Component {
render() {
return (
<div>
<Test {...this.props} />
</div>
);
}
}
class App extends Component {
constructor() {
super();
this.state = {
navigationTab: null,
};
}
hideTestDemo = false;
switchTab = (buttonKey) => {
if (this.hideTestDemo)
this.setState(prevState => ({
navigationTab: buttonKey,
hideTestDemo: true,
}));
else
this.setState(prevState => ({
navigationTab: buttonKey,
hideTestDemo:false,
}));
this.hideTestDemo=!this.hideTestDemo;
}
render() {
return (
<div>
<div>
<Button onClick={this.switchTab} buttonName='test' />
</div>
<AppContent {...this.state} />
</div>
);
}
}
render(<App />, document.getElementById('root'));

Rendering a dashboard with reusable components

I have to render several tabs on my dashboard, where I can navigate between them. Each tab must be reusable. In my sample code, I have 2 tabs and if I click on a tab, the matching tab renders. I just don't know if I am reasoning in the right way. How can I do this? My current Code is this:
import React, {Component} from 'react';
function SelectTab(props) {
var tabs = ['Overview', 'Favorites'];
return (
<ul>
{tabs.map(function(tab){
return (
<li key={tab} onClick={props.onSelect.bind(null, tab)}>
{tab}
</li>
)
}, this)}
</ul>
)
}
function Content(props) {
return (
<div>Content {props.tab}</div>
)
}
export default class MainContent extends Component {
constructor(props){
super(props);
this.state = {
selectedTab: 'Overview'
}
this.updateTab = this.updateTab.bind(this);
}
componentDidMount() {
this.updateTab(this.state.selectedTab);
}
updateTab(tab){
this.setState(function(){
return {
selectedTab: tab
}
});
}
render(){
return (
<div>
<SelectTab selectedTab={this.state.selectedTab} onSelect={this.updateTab}/>
<Content tab={this.state.selectedTab}/>
</div>
)
}
}
You did almost good. But I would rather render Content in SelectTab component as its already getting selectedTab prop, this way you can render specific content based on that prop. Also:
componentDidMount() {
this.updateTab(this.state.selectedTab);
}
is not necessary as state is already set.
Refactored example:
import React, { Component } from 'react';
function SelectTab(props) {
var tabs = ['Overview', 'Favorites'];
return (
<div>
<ul>
{tabs.map(function (tab) {
return (
<li key={tab} onClick={props.onSelect.bind(null, tab)}>
{tab}
</li>
)
}, this)}
</ul>
<Content tab={props.selectedTab}/>
</div>
)
}
function Content(props) {
return <div>Content {props.tab}</div>
}
export default class MainContent extends Component {
constructor(props) {
super(props);
this.state = {
selectedTab: 'Overview'
};
this.updateTab = this.updateTab.bind(this);
}
updateTab(tab) {
this.setState(function () {
return {
selectedTab: tab
}
});
}
render() {
return <SelectTab selectedTab={this.state.selectedTab} onSelect={this.updateTab}/>;
}
}
Also keep in mind that with proper babel-transpiling your code could be much simplified like this:
import React, { Component } from 'react';
const TABS = ['Overview', 'Favorites'];
const SelectTab = ({ selectedTab, onSelect }) => (
<div>
<ul>
{
TABS.map(
tab => <li key={tab} onClick={() => onSelect(tab)}> {tab} </li>
)
}
</ul>
<Content tab={selectedTab}/>
</div>
);
const Content = ({ tab }) => <div>Content {tab}</div>;
export default class MainContent extends Component {
state = {
selectedTab: 'Overview'
};
updateTab = tab => this.setState(() => ({ selectedTab: tab }));
render() {
return <SelectTab selectedTab={this.state.selectedTab} onSelect={this.updateTab}/>;
}
}

Overriding behaviour of sibling components

I've been struggling with the concept of overriding the behaviour of components between sibling components (components that do not have a parent-child relationship).
I have an App component that renders a header and a content component. The header contains a button that will let a user navigate back (not for real in this example). Now I want the content component to override the back button's behaviour, for example if a user is editing a form and I want to pop up a modal.
The reason I want to do this is because I want to (optionally) control navigation from within the content component itself.
I have found this (see snippet) solution, but I feel like it's not the right way to handle this. I would like to have some advice on how to handle this situation.
A few side-notes:
I'm actually building a react-native app, but in the hope of reaching more people I've simplified it down to a react example. I'm using NavigatorExperimental for navigation.
I am using redux/redux-form
Any help is appreciated.
class NavigationHeader extends React.Component {
render() {
return (
<span onClick={this.props.goBack}>Go back!</span>
);
}
}
class Content extends React.Component {
componentDidMount() {
this.props.setBackButtonBehaviour(() => console.log("BackButton overridden from Content"));
}
componentWillUnmount() {
this.props.resetBackButtonBehaviour();
}
render() {
return (<div style={{background: "red"}}>Content</div>);
}
}
class Navigator extends React.Component {
constructor(props) {
super(props);
this.state = {
overrideHeaderBackButtonBehaviour: null
}
}
setBackButtonBehaviour(func) {
this.setState({overrideHeaderBackButtonBehaviour: func});
}
resetBackButtonBehaviour() {
this.setState({overrideHeaderBackButtonBehaviour: null});
}
defaultBackButtonBehaviour() {
console.log("Default back-button behaviour");
}
render() {
return (
<div>
<NavigationHeader goBack={this.state.overrideHeaderBackButtonBehaviour || this.defaultBackButtonBehaviour} />
<Content
setBackButtonBehaviour={this.setBackButtonBehaviour.bind(this)}
resetBackButtonBehaviour={this.resetBackButtonBehaviour.bind(this)}
/>
</div>
);
}
}
ReactDOM.render(<Navigator />, document.getElementById("app"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="app">
</div>
You can try something like this:
Header.js
import actions from './actions';
#connect(
// map state to props
(state, props) => ({
canNavigate: state.navigation.canNavigate,
}),
// map dispatch to props
{
goBack: actions.goBack,
showNavigationAlert: actions.showNavigationAlert,
}
)
class Header extends React.Component {
render() {
return (
{/* if 'canNavigate' flag is false and user tries to navigate, then show alert */}
<span
onClick={this.props.canNavigate ? this.props.goBack : this.props.showNavigationAlert}
>
Go back!
</span>
);
}
}
Content.js
import actions from './actions';
#connect(
// map state to props
(state, props) => ({
navigationAlert: state.navigation.navigationAlert,
}),
// map dispatch to props
{
toggleNavigation: actions.toggleNavigation,
}
)
class Content extends React.Component {
render() {
return (
<div>
{/*
Show modal if 'navigationAlert' flag is true;
From the modal, if user choose to leave page then call toggleNavigation.
You may call the corresponding navigation function after this (eg: goBack)
*/}
{this.props.navigationAlert && <Modal toggleNavigation={this.props.toggleNavigation} />}
{/* eg: disable navigation when clicked inside the div */}
<form onClick={this.props.toggleNavigation}>
Content click!
</form>
</div>
);
}
}
App.js
class App extends React.Component {
render() {
return (
<div>
<Header />
<Content />
</div>
);
}
}
navigationReducer.js
const initialState = {
canNavigate: true,
navigationAlert: false,
};
export function navigationReducer(state = initialState, action = {}) {
let newState = {};
switch (action.type) {
case TOGGLE_NAVIGATION:
return { ...state, canNavigate: !state.canNavigate };
case TOGGLE_NAVIGATION_ALERT:
return { ...state, navigationAlert: !state.navigationAlert };
default:
return state;
}
}
actions.js
export function toggleNavigation(gdsSession) {
return { type: TOGGLE_NAVIGATION };
}
export function toggleNavigationAlert(gdsSession) {
return { type: TOGGLE_NAVIGATION_ALERT };
}
// ... other actions
and render into DOM.
ReactDOM.render(<App />, document.getElementById("app"));

Resources