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!
Slide Deck:
Questions?
brian.boyko@publicissapient.com
Well, it makes sense in context...
By brianboyko
Well, it makes sense in context...
- 547