Advanced React Workshop

Richard Lindsey     @Velveeta

Lesson 1

Render Callbacks

Render Callbacks

  • Allow separation of business logic from presentation
  • Useful pattern for both visual and service components
  • Define the core of what your component wants governance over, and inject those aspects into functions that return the view
  • Render function can be either a standard prop or the 'children' prop if passed between the tags

Render Callbacks

const ResizeObserver = class extends React.Component {
  _resizeObserver = new ResizeObserverPolyfill((entries) => {
    const [entry] = entries;

    this._updateFromDOMRect(entry.contentRect);
  });

  state = {
    height: null,
    ref: React.createRef(),
    width: null,
  };

  componentDidMount() {
    this._resizeObserver.observe(this.state.ref.current);

    this._updateFromDOMRect(this.state.ref.current.getBoundingClientRect());
  }

  componentWillUnmount() {
    this._resizeObserver.disconnect();
  }

  _updateFromDOMRect(rect) {
    this.setState({
      height: rect.height,
      width: rect.width,
    });
  }

  render() {
    return this.props.children(this.state);
  }
};

Render Callbacks

const MyResizeableComponent = () => (
  <ResizeObserver>
    {({ height, ref, width }) => (
      <div ref={ref}>
        My dimensions are {width}x{height} pixels!
      </div>
    )}
  </ResizeObserver>
);

Lesson 2

Data and Service Providers

Data and Service Providers

  • Using components as idioms to express your data and service needs helps your application read more intuitively
  • React's context Providers and Consumers can allow you to publish data and request functions to any number of downstream consumers, cache strategies, pooling, etc
  • Component-based services allow for centralizing maintenance concerns and simple drop-in consumption of services

Data and Service Providers

const UserDataContext = React.createContext();

const { Provider } = UserDataContext;

const UserDataProvider = class extends React.Component {
  _poolingTimeoutId = null;
  _queuedRequestIds = [];
  
  state = {
    users: {},
  };
  
  componentWillUnmount() {
    if (this._poolingTimeoutId) {
      clearTimeout(this._poolingTimeoutId);
    }
  }
  
  _issueRequest() {
    fetch('/users', {
      userIds: this._queuedRequestIds,
    })
    .then(resp => resp.json())
    .then(users => {
      this.setState(state => ({
        ...state,
        users: users.reduce((acc, user) => {
          acc[user.userId] = user;
          return acc;
        }, { ...state.users }),
      }))
    });
    
    this._queuedRequestIds = [];
  }
  
  _request = (userId) => {
    if (!this.state.users[userId]) {
      this._queuedRequestIds.push(userId);
    
      if (!this._poolingTimeoutId) {
        this._poolingTimeoutId = setTimeout(() => {
          this._poolingTimeoutId = null;
          this._issueRequest();
        }, this.props.poolingTimeout);
      }
    }
  };
  
  render() {
    const value = {
      request: this._request,
      users: this.state.users,
    }
    return <Provider value={value}>{this.props.children}</Provider>;
  }
};

Data and Service Providers

const UserDataConsumer = class extends React.Component {
  static contextType = UserDataContext;
  
  componentDidMount() {
    this.context.request(this.props.userId);
  }
  
  componentDidUpdate(prevProps) {
    if (prevProps.userId !== this.props.userId) {
      this.context.request(this.props.userId);
    }
  }
  
  render() {
    const user = this.context.users[this.props.userId];
    
    const props = {
      isLoading: !user,
      user,
    };

    return this.props.children(props);
  }
};

const UserInfo = ({ userId }) => (
  <UserDataConsumer userId={userId}>
    {({ isLoading, user }) => (
      {isLoading
        ? <LoadingSpinner />
        : <UserProfile user={user} />}
    )}
  </UserDataConsumer>
);

const UserList = ({ userIds }) => (
  <React.Fragment>
    {userIds.map(userId => (
      <UserInfo userId={userId} />
    ))}
  </React.Fragment>
);

Data and Service Providers

const UserDataConsumer = ({ children, userId }) => {
  const { request, users } = useContext(UserDataContext);
  
  useEffect(() => {
    request(userId);
  }, [request, userId]);

  const user = users[userId];
    
  const props = {
    isLoading: !user,
    user,
  };

  return children(props);
};

Data and Service Providers

const useUserData = (userId) => {
  const { request, users } = useContext(UserRestfulResourceFetcherContext);
  
  useEffect(() => {
    request(userId);
  }, [request, userId]);
  
  return users[userId];
};

const UserDataConsumer = ({ children, userId }) => {
  const user = useUserData(userId);

  const props = {
    isLoading: !user,
    user,
  };

  return children(props);
};

Lesson 3

Error Boundaries

Error Boundaries

  • Allow your application to recover from otherwise catastrophic errors by isolating the scope that errors can impact
  • Only operate during the render phase, but that can be worked around by using setState to throw async errors you specifically want to handle with your error boundary
  • Can be set up to automatically log errors to external services, and to render friendly error messages to your users

Lesson 4

Higher-order Components

  • Allow you to separate business logic or mixin feature sets from presentational components that might make use of it
  • Work in the form of higher-order functions that return new Component classes
  • Example: connect function from react-redux
  • Potential for prop name collisions if not handled thoughtfully

Higher-order Components

Higher-order Components

import React from 'react';

import SomeService from 'services/some-service';

const withSomeFeature = Component => {
  const ComponentWithSomeFeature = class extends React.Component {
    static displayName = `SomeFeature(${Component.displayName})`;
    
    state = {
      someFeatureResult: null;
    }
    
    componentDidMount() {
      this._computeSomeFeatureResult();
    }
    
    componentDidUpdate() {
      this._computeSomeFeatureResult();
    }
    
    _computeSomeFeatureResult() {
      this.setState({
        someFeatureResult: SomeService.getResults(this.props),
      });
    }
    
    render() {
      return <Component someFeature={this.state.someFeatureResult} {...this.props} />;
    }
  };
  
  return ComponentWithSomeFeature;
}

export default withSomeFeature;

Higher-order Components

import React from 'react';

import withSomeFeature from 'hocs/with-some-feature';

const MyComponent = ({ children, someFeature }) => {
  if (!someFeature) {
    return null;
  }
  
  return (
    <div>
      <div>Some feature is {someFeature}!</div>
      {children}
    </div>
};

export default withSomeFeature(MyComponent);

Lesson 5

React Hooks

React Hooks

  • Only available for functional components
  • Function names must start with the word "use"
  • Allow you to abstract useful service-related functionality into simple function calls, and test that functionality in one place
  • Highly composable, just like the React components they serve
  • May only run during the render cycle, but provide a couple of escape hatches for asynchronous needs
  • When used properly, hooks have a very "mixin" feel without the drawbacks of using actual mixin patterns (name collisions, tight coupling, etc)
import React from 'react';

const MyStatefulComponent = class extends React.Component {
  _ref = React.createRef();
  
  state = {
    isOpen: this.props.isOpen,
  };

  componentDidMount() {
    this.setState({ isOpen: this.props.isOpen });
    
    if (this.props.isOpen) {
      document.addEventListener('click', this._documentClick);
    }
  }
  
  componentDidUpdate(prevProps, prevState) {
    if (prevProps.isOpen !== this.props.isOpen) {
      this.setState({ isOpen: this.props.isOpen });
    }
    
    if (prevState.isOpen !== this.state.isOpen) {
      if (this.state.isOpen) {
        document.addEventListener('click', this._documentClick);
      } else {
        document.removeEventListener('click', this._documentClick);
      }
    }
  }
  
  componentWillUnmount() {
    if (this.state.isOpen) {
      document.removeEventListener('click', this._documentClick);
    }
  }

  _documentClick = (e) => {
    if (!this._ref.current.contains(e.target)) {
      this.setState({ isOpen: false });
    }
  };

  _onSelect = (e) => {
    this.props.onChange(e.target.dataset.value);
    this.setState({ isOpen: false });
  };
  
  render() {
    const {
      options,
      value,
    } = this.props;

    return (
      <div>
        <div className="dropdown-value">{value}</div>
        {isOpen && (
          <div className="dropdown-list" ref={this._ref}>
            {options.map(option => {
              <div data-value={option.value} key={option.value} onClick={this._onSelect}>
                {option.display}
              </div>
            })}
          </div>
        )}
      </div>
    );
  }
}

React Hooks

React Hooks

import React, { useCallback, useEffect, useRef, useState } from 'react';

const MyStatefulFunctionalComponent = ({
  isOpen: isOpenProp,
  onChange,
  options,
  value,
}) => {
  const ref = useRef();
  const [isOpen, setIsOpen] = useState(isOpenProp);
  
  useEffect(() => {
    if (isOpen) {
      const documentClick = (e) => {
        if (!ref.current.contains(e.target)) {
          setIsOpen(false);
        }
      };
    
      document.addEventListener('click', documentClick);
      
      return () => {
        document.removeEventListener('click', documentClick);
      };
    }
  }, [isOpen]);
  
  useEffect(() => {
    setIsOpen(isOpenProp);
  }, [isOpenProp]);
  
  const onSelect = useCallback((e) => {
    onChange(e.target.dataset.value);
    setIsOpen(false);
  }, [onChange]);
  
  return (
    <div>
      <div className="dropdown-value">{value}</div>
      {isOpen && (
        <div className="dropdown-list" ref={ref}>
          {options.map(option => {
            <div data-value={option.value} key={option.value} onClick={onSelect}>
              {option.display}
            </div>
          })}
        </div>
      )}
    </div>
  );
};

Lesson 6

React Refs

  • The proper way to deal with rendered elements
  • Enable you to get a reference to a component instance or an actual DOM element
  • Enable you to get a reference to a DOM element
  • React.forwardRef for functional components to expose underlying semantic details to consumers

React Refs

Lesson 7

React.Suspense and React.lazy

React.Suspense and React.lazy

  • Built-in methods for dealing with asynchronous loading and rendering of components
  • React.lazy is built for dynamic imports (code splitting)
  • React.Suspense is built to render fallback content while waiting for asynchronous code chunks to load

React.Suspense and React.lazy

import React, { lazy, Suspense } from 'react';

import LoadingSpinner from '../components/loading-spinner';
import Modal from '../components/modal';
import Router from '../router';
import TermsAndServicesLink from '../components/tos-link';

const TermsAndServices = lazy(() => import('../legal/tos'));

const Layout = () => {
  const [showTos, setShowTos] = useState(false);
  
  return (
    <React.Fragment>
      <main>
        <Router />
      </main>
      <footer>
        <TermsAndServicesLink onClick={() => setShowTos(true)} />
        {showTos && (
          <Modal onClose={() => setShowTos(false)}>
            <Suspense fallback={<LoadingSpinner />}>
              <TermsAndServices />
            </Suspense>
          </Modal>
        )}
      </footer>
    </div>
  );
};

export default Layout;

Concurrent Rendering Mode

Coming Soon!

Concurrent Rendering Mode

  • Currently, React renders synchronously, and reconciling large component trees can tie up the main thread, leading to sluggish user interactions and dropped frames
  • Time slicing feature will allow the reconciliation cycle to be divided into work units and processed based on priority
    • User interactions have highest priority
    • State updates can have assigned priority levels to dictate which updates are more important than others
  • React may choose to pause, abandon, or restart partially-reconciled component trees based on incoming events and priority settings

Concurrent Rendering Mode

Concurrent Rendering Mode

Concurrent Rendering Mode

fn.

Day 2 complete, congratulations!

Richard Lindsey     @Velveeta

Advanced React Workshop

By Richard Lindsey

Advanced React Workshop

  • 161