React'ing with Context, Refs and Hooks

Author: Steve Venzerul

The new Hooks API

Hooks are a new addition in React 16.8. They let you use state and other React features without writing a class.

- React Documentation

Let's unpack that description!

import React, { useState } from 'react';

export default class Button extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      count: 0,
    };

    if (props.handleClick) {
      this.handleClick = props.handleClick;
    }
  }

  handleClick = () => {
    // this.setState({count: ++count}); // normally a bug!
    this.setState(state => {
      return { count: ++state.count };
    });
  };

  render() {
    return (
      <button className="generic-button" onClick={this.handleClick}>
        Click count: {this.state.count}
      </button>
    );
  }
}

Traditional Button

export function HookButton() {
  let [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(++count);
  };

  return (
    <button className="generic-button" onClick={handleClick}>
      Click count: {count}
    </button>
  );
}

Button Using Hooks

function getFormattedTime(date) {
  return date
    .toISOString()
    .replace('T', ' ')
    .replace('Z', '');
}

export function MultiHookButton() {
  let [count, setCount] = useState(0);
  let [when, setWhen] = useState(null);

  const handleClick = () => {
    setCount(++count);
    setWhen(new Date());
  };

  return (
    <>
      <div>
        Last updated: {(when && getFormattedTime(when)) || 'Not updated'}
      </div>
      <button className="generic-button" onClick={handleClick}>
        Click count: {count}
      </button>
    </>
  );
}

Multiple States

useState call order is important!

Hook Rules

The Effect Hook

import React, { useState, useEffect } from 'react';
import * as util from '../util';

let messageLoadingInt;

function loadMessages(cb) {
  messageLoadingInt = setInterval(() => {
    console.log('Refreshing messages...');

    return cb([
      { id: util.monotonic(), content: 'some message', user: 'Dan' },
      { id: util.monotonic(), content: 'another message', user: 'me' },
    ]);
  }, 3000);
}

export default function Messages({ initialMessages = [] }) {
  let [messages, setMessages] = useState(initialMessages);

  useEffect(() => {
    loadMessages(data => {
      setMessages(data);
    });

    return () => {
      clearInterval(messageLoadingInt);
    };
  });

  let children = [];
  for (let msg of messages) {
    children.push(
      <li key={msg.id}>
        {msg.user}: {msg.content}
      </li>
    );
  }

  return (
    <>
      <div className="message-list-header">Messages</div>
      <ul className="message-list">{children}</ul>
    </>
  );
}

A Message List Component

Important things to remember

  • The useEffect function will be run on each render/re-render and during component unmount. Expensive cleanup should be strictly avoided.
  • setInterval and setTimeout inside useEffect are really tricky to get right, use Abramov's useInterval if you don't feel comfortable implementing this.

Refs and ForwardRef

In the typical React dataflow, props are the only way that parent components interact with their children. To modify a child, you re-render it with new props. However, there are a few cases where you need to imperatively modify a child outside of the typical dataflow. The child to be modified could be an instance of a React component, or it could be a DOM element. For both of these cases, React provides an escape hatch.

- React Documentation

Use Cases

 

  1. Managing focus, text selection, or media playback.
  2. Triggering imperative animations.
  3. Integrating with third-party DOM libraries.
  4. Working with React portals.
  5. Performing calculations on DOM elements.
class App extends React.Component {
  constructor(props) {
    super(props);
    this.modalInputRef = React.createRef();
    this.onClick = this.onClick.bind(this);
  }

  onClick() {
    console.log(this.modalInputRef);
    this.modalInputRef.current.value = 'value from parent.';
  }
  
  render() {
    return (
      <div>
        <button onClick={this.onClick}>Set focus</button>
        <ModalPortal ref={this.modalInputRef} />
      </div>
    );
  }
}

class Portal extends React.Component {
  constructor(props) {
    super(props);
    this.el = document.createElement('div');
  }

  componentDidMount() {
    const root = document.querySelector('#app');
    this.el.className = 'portal-x';
    root.appendChild(this.el)
  }

  render() {
    return ReactDOM.createPortal(<input type='text' class='modal-input' ref={this.props.inputRef} />, this.el)
  }
}

const ModalPortal = React.forwardRef((props, ref) => {
  return <Portal inputRef={ref} {...props} />
});

ReactDOM.render(<App />, document.querySelector("#app"))

Fiddle

Contexts

Use Cases

  1. Passing props through deeply nested component hierarchies.
  2. Propagating global changes across those hierarchies.
  3. Light weight substitute for using libraries like Redux, Mobx and friends.
const UserContext = React.createContext({
  loggedIn: false,
  login: () => {}
});

class App extends React.Component {
  constructor(props) {
    super(props);
    this.login = this.login.bind(this);
    
    this.state = {
      loggedIn: false,
      login: this.login
    };
  }
  
  login() {
    this.setState({
      loggedIn: !this.state.loggedIn
    });
  }
  
  render() {
    return (
      <UserContext.Provider value={this.state}>
        <Toolbar />
      </UserContext.Provider>
    );
  }
}

function Toolbar(props) {
  return (
    <div>
      <LoginButton />
    </div>
  );
}

class LoginButton extends React.Component {
  login() {
    this.context.login();
  }
  
  render() {
    return (
      <div>
        <span>User is logged {this.context.loggedIn ? 'In' : 'Out'}</span><br />
        <button onClick={this.context.login}>{this.context.loggedIn ? 'Logout' : 'Login'}</button>
      </div>
    );
  }
}

LoginButton.contextType = UserContext;

ReactDOM.render(<App />, document.querySelector("#app"));

Fiddle

React Gotch'ya

class Child extends React.Component {
	render() {
  	let out = [];
    const {name, bag} = this.props;
   	console.log('rendered', name);
  	
  	return <div>
      <h3>-- {name}</h3>
  	  {Object.entries(bag).map(([k, v]) => <div>{k}: {v.toString()}</div>)}
  	</div>
  }
}

class Parent extends React.Component {
	constructor(props) {
  	super(props);
    this.update = this.update.bind(this);
    
  	this.state = {
    	child1: {
      	cp1: 'v1',
        cp2: 'v2',
      },
      
      child2: {
      	p1: 'v1',
        p2: {}
      }
    }
  }
  
  update() {
  	this.setState({
    	child1: {
      	p1: 'new-val'
      }
    })
  }
  
	render() {
  	return (<div>
      <Child bag={this.state.child1} name='child1' />
      <br />
      <Child bag={this.state.child2} name='child2' />
      <br />
      <button onClick={this.update}>Update state</button>
  	</div>);
  }
}

ReactDOM.render(<Parent />, document.querySelector('#app'));

Fiddle

Questions?

Resources

Made with Slides.com