Well, it makes sense in Context...

Brian Boyko

Sr. Experience Technology Engineer

at Publicis Sapient London

@boyko4tx

A Brief History of React -

React.CreateClass

// my old HackReactor Thesis Project, five years ago. 
// I AM INCREDIBLY ASHAMED.  
// we were still on ES5 syntax...

var Main = React.createClass({
  handleClick: function(){
  	// ugh, jQuery in a React app. This is criminal.  
    window.jeopardy.username = $('#username').val();
    window.jeopardy.code     = $('#code').val().toUpperCase();
    if (window.jeopardy.username.length < 1) {
      alert("Please enter a username.");
    } else {
      socket.emit('student-join',{username:window.jeopardy.username, code:window.jeopardy.code});
    }
  },
  render: function(){
    socket.on('you-joined', function(){
      console.log("you joined!");
      React.render( <div> <Waiting /> <Buzzer /> </div>, document.getElementById('main') )
    })
    socket.on('no-game', function(){alert("No such game. Please try another game code.")})
    return (
      <div className="signin">
        <h1>Sign in to play:</h1>
        <label>Username: </label>
        <input type="text" className="input" placeholder="Choose a Username" id="username" />
        <label>Code: </label>
        <input type="text" className="input" placeholder="Enter Your Code" id="code" />
        
        <a onClick={this.handleClick} className="waves-effect waves-light btn-large">
            <i className="material-icons right">play_arrow</i>Join Game
        </a>
        <div id="status"></div>
      </div>
    )
  }
})

A Brief History of React: React.Component

import React, { Component } from 'react';
import { BrowserRouter, Route, Switch, Redirect } from 'react-router-dom';
import './App.css';
import Category from './Category';
import Header from './Header';
import Cart from './Cart';
import Product from './Product';
import { actions } from './store';

class App extends Component {
  componentWillMount() {
    if (!this.props.platesIsLoaded) {
      this.props.getPlates();
    }
  }
  render() {
    return this.props.platesIsLoaded ? (
      <div className="App">
        <BrowserRouter>
          <div>
            <Header />
            <Switch>
              <Route path="/product/:productId" component={Product} />
              <Route path="/category/:category" component={Category} />
              <Route path="/cart" component={Cart} />
              <Route exact path="/">
                <Redirect to="/category/plates" />
              </Route>
              <Route>
                <Redirect to="/category/plates" />
              </Route>
            </Switch>
          </div>
        </BrowserRouter>
      </div>
    ) : null;
  }
}

const mapStateToProps = state => {
  return { platesIsLoaded: !!get(state, 'categories.plates', false) };
};

/* This code would likely not be in a real application in this component.
   See /src/Category/index.js for more information */
const mapDispatchToProps = dispatch => ({
  getPlates: () => dispatch(actions.getCategory('plates'))
});

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(App);

A Brief History of React: React.FC

import React from 'react';
import { withRouter } from 'react-router-dom';

const ProductBreadcrumbs = props => (
  <div className="product__detail__breadcrumbs">
    <span
      className="product__detail__breadcrumbs__link"
      onClick={() => props.history.push('/')}
    >
      {'HOME'}
    </span>
    {` / `}
    <span
      className="product__detail__breadcrumbs__link"
      onClick={() => props.history.push(`/category/${props.category}`)}
    >
      {props.category}
    </span>
    {` / `}
    <span className="product__detail__breadcrumbs__title">{props.title}</span>
  </div>
);

export default withRouter(ProductBreadcrumbs);

Where we are now: Hooks

import React, { useState } from "react";

const SimpleCounter: React.FC<{}> = () => {
  const [count, setCount] = useState<number>(0);
  const increment = () => {
    setCount(prevCount => prevCount + 1);
  };
  const decrement = () => {
    setCount(prevCount => prevCount - 1);
  };
  const setToExactlySeven = () => {
    setCount(7); 
  }
  return (
    <div>
      <h1>{count}</h1>
      <div>
        <button onClick={increment}>Increment</button>
      </div>
      <div>
        <button onClick={decrement}>Decrement</button>
      </div>
      <div>
        <button onClick={setToExactlySeven}>Set count to exactly 7</button>
      </div>
    </div>
  );
};

export default SimpleCounter;

Custom React Hooks

Example: useSimpleCounter()

import React, { useState } from "react";

const SimpleCounter: React.FC<{}> = () => {
  const [count, setCount] = useState<number>(0);
  const increment = () => {
    setCount(prevCount => prevCount + 1);
  };
  const decrement = () => {
    setCount(prevCount => prevCount - 1);
  };
  const setToExactlySeven = () => {
    setCount(7);
  };
  return (
    <div className="demo-box">
      <h1>{count}</h1>
      <div>
        <button onClick={increment}>Increment</button>
      </div>
      <div>
        <button onClick={decrement}>Decrement</button>
      </div>
      <div>
        <button onClick={setToExactlySeven}>Set count to exactly 7</button>
      </div>
    </div>
  );
};

export default SimpleCounter;
import { useState } from "react";

const useSimpleCounter = () => {
  const [count, setCount] = useState<number>(0);
  const increment = () => {
    setCount(prevCount => prevCount + 1);
  };
  const decrement = () => {
    setCount(prevCount => prevCount - 1);
  };
  const setToExactlySeven = () => {
    setCount(7);
  };
  return { count, increment, decrement, setToExactlySeven };
};

export default useSimpleCounter;
import React from "react";
import useSimpleCounter from "../../hooks/useSimpleCounter";

const SimpleCounterWithHook: React.FC<{}> = () => {
  const { count, increment, decrement, setToExactlySeven } = useSimpleCounter();
  return (
    <div className="demo-box">
      <h1>{count}</h1>
      <div>
        <button onClick={increment}>Increment</button>
      </div>
      <div>
        <button onClick={decrement}>Decrement</button>
      </div>
      <div>
        <button onClick={setToExactlySeven}>Set count to exactly 7</button>
      </div>
    </div>
  );
};

export default SimpleCounterWithHook;

useSimpleCounter

import React from "react";
import useSimpleCounter from "../../hooks/useSimpleCounter";

interface SimpleCounterState {
  count: number;
  increment: () => void;
  decrement: () => void;
  setToExactlySeven: () => void;
}

export const SimpleCounterWithHook: React.FC<SimpleCounterState> = ({
  count,
  increment,
  decrement,
  setToExactlySeven
}) => {
  return (
    <div className="demo-box">
      <h1>{count}</h1>
      <div>
        <button onClick={increment}>Increment</button>
      </div>
      <div>
        <button onClick={decrement}>Decrement</button>
      </div>
      <div>
        <button onClick={setToExactlySeven}>Set count to exactly 7</button>
      </div>
    </div>
  );
};

const ConnectedSimpleCounterWithHook: React.FC<{}> = () => {
  const { count, increment, decrement, setToExactlySeven } = useSimpleCounter();
  return (
    <SimpleCounterWithHook
      {...{ count, increment, decrement, setToExactlySeven }}
    />
  );
};

export default ConnectedSimpleCounterWithHook;
import React from "react";
import SimpleCounterWithHook from "./SimpleCounterWithHook";

const ThreeSimpleCounters: React.FC<{}> = () => (
  <div>
    <SimpleCounterWithHook />
    <SimpleCounterWithHook />
    <SimpleCounterWithHook />
  </div>
);

export default ThreeSimpleCounters;

Custom React Hooks

Example: useViewport()

import { useEffect, useState } from "react";
import throttle from "lodash/throttle";

const THROTTLE_SPEED = 100;

export const useViewport = () => {
  const [pageYOffset, setYOffset] = useState<number>(0);
  const [innerWidth, setInnerWidth] = useState<number>(720);
  const [innerHeight, setInnerHeight] = useState<number>(480);

  useEffect(() => {
    const handleScroll = () => {
      setYOffset(window.pageYOffset);
    };

    const handleResize = (): void => {
      setInnerWidth(window.innerWidth);
      setInnerHeight(window.innerHeight);
    };

    const throttledHandleScroll = throttle(handleScroll, THROTTLE_SPEED);
    const throttledHandleResize = throttle(handleResize, THROTTLE_SPEED);
    throttledHandleScroll(); // run once to establish a base;
    throttledHandleResize();
    window.addEventListener("scroll", throttledHandleScroll);
    window.addEventListener("resize", throttledHandleResize);
    // The return will execute on unmount, cleaning up the listener.
    return () => {
      window.removeEventListener("scroll", throttledHandleScroll);
      window.removeEventListener("resize", throttledHandleResize);
    };
  }, []);

  return { pageYOffset, innerWidth, innerHeight };
};

export default useViewport;

Custom React Hooks

Example: useViewport()

// src/components/ComponentUsesViewport/ViewportBox.tsx
import React from "react";
import useViewport from "../../hooks/useViewport";

const ViewportBox: React.FC<{}> = () => {
  const { pageYOffset, innerWidth, innerHeight } = useViewport();
  return (
    <div className="viewport-box">
      <div>The pageYOffset is: {pageYOffset}</div>
      <div>The innerWidth is: {innerWidth}</div>
      <div>The innerHeight is: {innerHeight}</div>
    </div>
  );
};

export default ViewportBox;

ViewportBox.tsx

import React from "react";

import Lorem from "../Lorem";
import ViewportBox from "./ViewportBox";

const ComponentUsesViewport: React.FC<{}> = () => {
  return (
    <div>
      <Lorem />
      <ViewportBox />
      <Lorem />
    </div>
  );
};

export default ComponentUsesViewport;

ComponentUsesViewport.tsx

Custom Hooks (are not singletons)

let count: number = 0; 

export const useViewport = () => {
  const [pageYOffset, setYOffset] = useState<number>(0);
  const [innerWidth, setInnerWidth] = useState<number>(720);
  const [innerHeight, setInnerHeight] = useState<number>(480);

  useEffect(() => {
    count += 1;
    console.log(`Event listeners have been added ${count} times`)
    //... REST OF USE EFFECT
  }, []);

  return { pageYOffset, innerWidth, innerHeight };
};

export default useViewport;
const ComponentUsesMultipleViewports: React.FC<{}> = () => {
  return (
    <div>
      <Lorem />
      <ViewportBox />
      <ViewportBox />
      <ViewportBox />
      <Lorem />
    </div>
  );
};

From Custom Hooks
To the ContextAPI

and useContext()

import React, { createContext } from "react";
import noop from "lodash/noop";
import useSimpleCounter from "../hooks/useSimpleCounter";

interface SimpleCounterState {
  count: number;
  increment: () => void;
  decrement: () => void;
  setToExactlySeven: () => void;
}

const initialState: SimpleCounterState = {
  count: 0,
  increment: noop,
  decrement: noop,
  setToExactlySeven: noop
};

export const SimpleCounterContext = createContext<SimpleCounterState>(
  initialState
);

export const SimpleCounterProvider: React.FC<{ children: any }> = ({
  children
}) => {
  const simpleCounter: SimpleCounterState = useSimpleCounter();
  return (
    <SimpleCounterContext.Provider value={simpleCounter}>
      {children}
    </SimpleCounterContext.Provider>
  );
};

export default SimpleCounterContext;
// ThreeConnectedCounters.tsx
import React, { useContext } from "react";
import SimpleCounterContext, {
  SimpleCounterProvider
} from "../../contexts/SimpleCounterContext";
import { SimpleCounterWithHook } from "./SimpleCounterWithHook";

const ThreeConnectedCounters: React.FC<{}> = () => {
  const simpleCounter = useContext(SimpleCounterContext);
  return (
    <div>
      <SimpleCounterWithHook {...simpleCounter} />
      <SimpleCounterWithHook {...simpleCounter} />
      <SimpleCounterWithHook {...simpleCounter} />
    </div>
  );
};

const ConnectedThreeConnectedCounters: React.FC<{}> = () => (
  <SimpleCounterProvider>
    <ThreeConnectedCounters />
  </SimpleCounterProvider>
);

export default ConnectedThreeConnectedCounters;

There is one caveat...

Any component that uses a context will re-render when the context changes.

import React, { createContext } from "react";
import noop from "lodash/noop";
import useSimpleCounter from "../hooks/useSimpleCounter";
import useViewport from "../hooks/useViewport";

interface OverloadedContextState {
  count: number;
  increment: () => void;
  decrement: () => void;
  setToExactlySeven: () => void;
  pageYOffset: number;
  innerWidth: number;
  innerHeight: number;
}

const initialState: OverloadedContextState = {
  count: 0,
  increment: noop,
  decrement: noop,
  setToExactlySeven: noop,
  pageYOffset: 0,
  innerWidth: 0,
  innerHeight: 0
};

export const OverloadedContext = createContext<OverloadedContextState>(
  initialState
);

export const OverloadedContextProvider: React.FC<{ children: any }> = ({
  children
}) => {
  const viewportState = useViewport();
  const simpleCounterState = useSimpleCounter();
  const overloaded: OverloadedContextState = {
    ...viewportState,
    ...simpleCounterState
  };
  return (
    <OverloadedContext.Provider value={overloaded}>
      {children}
    </OverloadedContext.Provider>
  );
};

export default OverloadedContext;

src/contexts/OverloadedContext.tsx

import React, { useContext } from "react";
import OverloadedContext, {
  OverloadedContextProvider
} from "../../contexts/OverloadedContext";
import { SimpleCounterWithHook } from "../SimpleCounter/SimpleCounterWithHook";
import Lorem from "../Lorem";

let timesTheComponentHasRerendered: number = 0;

const OverloadedContextComponent: React.FC<{}> = () => {
  const { count, increment, decrement, setToExactlySeven } = useContext(
    OverloadedContext
  );
  timesTheComponentHasRerendered += 1;
  console.log(
    `Times the component has Rerendered: ${timesTheComponentHasRerendered}`
  );
  return (
    <div>
      <SimpleCounterWithHook
        {...{ count, increment, decrement, setToExactlySeven }}
      />
      <Lorem />
    </div>
  );
};

const ConnectedOverloadedContextComponent: React.FC<{}> = () => (
  <OverloadedContextProvider>
    <OverloadedContextComponent />
  </OverloadedContextProvider>
);

export default ConnectedOverloadedContextComponent;

src/Components/OverloadedContextComponent.tsx

Solving the Overloaded Context Problem

Multiple ways:

Solution #1

Use Redux.

Solution #2

Create (and nest) multiple contexts

import React, { createContext } from "react";
import useViewport from "../hooks/useViewport";

interface ViewportState {
  innerWidth: number;
  innerHeight: number;
  pageYOffset: number;
}

const initialState: ViewportState = {
  innerWidth: 0,
  innerHeight: 0,
  pageYOffset: 0
};

export const ViewportContext = createContext<ViewportState>(initialState);

export const ViewportContextProvider: React.FC<{ children: any }> = ({
  children
}) => {
  const viewport: ViewportState = useViewport();
  return (
    <ViewportContext.Provider value={viewport}>
      {children}
    </ViewportContext.Provider>
  );
};

export default ViewportContext;

./src/contexts/ViewportContext.tsx

let simpleCounterRerenders: number = 0;
let viewportRerenders: number = 0;

const SeperatedCounterComponent: React.FC<{}> = () => {
  const { count, increment, decrement, setToExactlySeven } = useContext(
    SimpleCounterContext
  );
  simpleCounterRerenders += 1;
  console.log(`SimpleCounter Rerenders: ${simpleCounterRerenders}`);
  return (
    <div>
      <SimpleCounterWithHook
        {...{ count, increment, decrement, setToExactlySeven }}
      />
    </div>
  );
};

const SeperatedViewpointComponent: React.FC<{}> = () => {
  const { pageYOffset, innerWidth, innerHeight } = useContext(ViewportContext);
  viewportRerenders += 1;
  console.log(`Viewport Rerenders: ${viewportRerenders}`);

  return (
    <div className="viewport-box">
      <div>The pageYOffset is: {pageYOffset}</div>
      <div>The innerWidth is: {innerWidth}</div>
      <div>The innerHeight is: {innerHeight}</div>
    </div>
  );
};

SeperatedContextComponent.tsx (just the components)

 

const OldAndBustedConnector: React.FC<{ children: any }> = ({ children }) => (
  <SimpleCounterProvider>
    <ViewportContextProvider>{children}</ViewportContextProvider>
  </SimpleCounterProvider>
);

const Connector: React.FC<{ children: any; Providers: any[] }> = ({
  children,
  Providers
}) => {
  return Providers.reduce(
    (prev, Provider) => <Provider>{prev}</Provider>,
    children
  );
};

const ConnectedComponents: React.FC<{}> = () => {
  return (
    <Connector Providers={[ViewportContextProvider, SimpleCounterProvider]}>
      <div>
        <Lorem />
        <SeperatedCounterComponent />
        <SeperatedViewpointComponent />
        <Lorem />
      </div>
    </Connector>
  );
};

export default ConnectedComponents;

SeperatedContextComponent.tsx (the cool stuff)

 

Thank you!

Questions?

brian.boyko@publicissapient.com

Well, it makes sense in context...

By brianboyko

Well, it makes sense in context...

  • 547