React refactoring, off the hook(s)!

A presentation at jsDay in May 2019 in Verona, VR, Italy by Marco Cedaro

Slide 1

Slide 1

REFACTORING OFF THE HOOKS

Slide 2

Slide 2

YOU MIGHT HAVE HEARD OF REACT https://evilmartians.com/chronicles/optimizing-react-virtual-dom-explained

Slide 3

Slide 3

A JAVASCRIPT LIBRARY FOR BUILDING USER INTERFACES

  1. DECLARATIVE, 2. COMPONENT-BASED, 3. LEARN ONCE, WRITE EVERYWHERE react docs

Slide 4

Slide 4

A JAVASCRIPT LIBRARY FOR BUILDING USER INTERFACES

  1. DECLARATIVE, 2. COMPONENT-BASED, 3. LEARN ONCE, WRITE EVERYWHERE react docs

Slide 5

Slide 5

PROPS STATE https://thenounproject.com/indygo/collection/hand-drawn-arrows-4/

Slide 6

Slide 6

A SIMPLE FUNCTION COMPONENT const HelloMessage = (props) =>” ( <div> Hello {props.name} Type a quote here. </div> ); ReactDOM.render( <HelloMessage name=”Taylor” />, document.getElementById(‘hello-example’) );

Slide 7

Slide 7

A SIMPLE CLASS COMPONENT class HelloMessage extends React.Component { render() { return ( <div> Hello {this.props.name} Type a quote here. </div> ); } } ReactDOM.render( <HelloMessage name=”Taylor” />, document.getElementById(‘hello-example’) );

Slide 8

Slide 8

NEITHER MODEL REALLY CAPTURES REACT.

Slide 9

Slide 9

https://dan.church

Slide 10

Slide 10

WHAT’S A COMPONENT WHY ARE THESE MODELS INSUFFICIENT TO DESCRIBE REACT? dan abramov

Slide 11

Slide 11

WHAT’S A COMPONENT “PURE FUNCTION” MODEL DOESN’T DESCRIBE LOCAL STATE WHICH IS AN ESSENTIAL REACT FEATURE. dan abramov

Slide 12

Slide 12

WHAT’S A COMPONENT “CLASS” MODEL DOESN’T EXPLAIN PURE-ISH RENDER, DISAWOVING INHERITANCE, LACK OF DIRECT INSTANTIATION, AND “RECEIVING” PROPS. dan abramov

Slide 13

Slide 13

🤦 😬 TWITTER EMAIL: marco@cedmax cedmax.com NAME WEBSITE PRONOUNS: HE/HIM 🤷 😅

Slide 14

Slide 14

WE’RE ARE GOING TO TALK ABOUT COLOURS

Slide 15

Slide 15

Slide 16

Slide 16

Slide 17

Slide 17

Slide 18

Slide 18

Slide 19

Slide 19

COMPONENT STRUCTURE export default class App extends Component { state = { colors: originalList, currentFilter: “”, currentSortBy: “name”, style: { …constants, color: “black”, background: “white” }, }; Type a quote here. sortBy = sortBy =>” { this.setState({ currentSortBy: sortBy, colors: sorterssortBy, }); }; filter = currentFilter=>” { const colors = getFilteredColours(originalList, currentFilter); this.setState({ currentFilter, colors }); };

Slide 20

Slide 20

COMPONENT STRUCTURE onColorChange = hex =>” { this.setState({ style: { …this.state.style, background: hex, color: getMostReadable(hex), }, Type a quote here. }); }; return ( <AppUI {…state} sortBy={sortBy} filter={filter} onColorChange={onColorChange} /> ); };

Slide 21

Slide 21

LET’S CODE

Slide 22

Slide 22

HOOKS

Slide 23

Slide 23

A JAVASCRIPT LIBRARY FOR BUILDING USER INTERFACES

  1. OPT-IN 2. 100% BACKWARDS-COMPATIBLE 3. AVAILABLE NOW HOOKS WERE RELEASED WITH REACT V16.8.0. react docs

Slide 24

Slide 24

USESTATE

Slide 25

Slide 25

USESTATE DEFAULT VALUE VALUE const [colors, setColors] = useState(originalList); ARRAY FUNCTION TO SET THE STATE

Slide 26

Slide 26

USESTATE DEFAULT VALUE VALUE const [colors, setColors] = useState(originalList); ARRAY FUNCTION TO SET THE STATE CAN TAKE A FUNCTION… setColors(sortingFunction);

Slide 27

Slide 27

USESTATE DEFAULT VALUE VALUE const [colors, setColors] = useState(originalList); ARRAY FUNCTION TO SET THE STATE CAN TAKE A FUNCTION… setColors(sortingFunction); … AND SO DOES USE STATE

Slide 28

Slide 28

USESTATE DEFAULT VALUE VALUE const [colors, setColors] = useState(originalList); ARRAY FUNCTION TO SET THE STATE CAN TAKE A FUNCTION… … AND SO DOES USE STATE setColors(sortingFunction); setColors(sortingFunction(colors)); THEY ARE EXACTLY THE SAME

Slide 29

Slide 29

USESTATE setCurrentFilter(currentFilter); setColors(colors); COMPONENTS UPDATES GET ENQUEUED

Slide 30

Slide 30

USECALLBACK

Slide 31

Slide 31

USECALLBACK WRAPS A FUNCTION RETURNING A MEMOIZED VERSION const sortBy = useCallback(e =>” { const { value: sortBy } = e.target; const sortingFunction = sorters[sortBy]; setColors(sortingFunction); }, []);

Slide 32

Slide 32

USEREDUCER

Slide 33

Slide 33

STILL QUITE BUSY export default () =>” { const [colors, setColors] = useState(originalList); const [currentFilter, setCurrentFilter] = useState(“”); const [currentSortBy, setCurrentSortBy] = useState(“name”); const [style, setStyle] = useState({ …constants, color: “black”, background: “white”, }); const sortBy = useCallback(sortBy =>” { const sortingFunction = sorters[sortBy]; setCurrentSortBy(sortBy); setColors(sortingFunction); }, []); const filter = useCallback(currentFilter =>” { const colors = getFilteredColors(originalList, currentFilter); setCurrentFilter(currentFilter); setColors(colors); }, []);

Slide 34

Slide 34

STILL QUITE BUSY const onColorChange = useCallback(hex =>” { setStyle({ …style, background: hex, color: getMostReadable(hex), }); }, []); return ( <AppUI {…state} sortBy={sortBy} filter={filter} onColorChange={onColorChange} /> ); };

Slide 35

Slide 35

USEREDUCER VALUE DEFAULT VALUE const [state, dispatch] = useReducer(reducer, defaultState); ARRAY REDUCERS

Slide 36

Slide 36

USEREDUCER export default (state, { type, payload }) =>” { switch (type) { case “filter”: return { …state, currentFilter: payload, colors: getFilteredColors(state.allColors, payload), }; case “sort”: return { …state, currentSortBy: payload, colors: sorterspayload, }; case “change”: return { …state, style: { …state.style, background: payload, color: getMostReadable(payload), }, }; default: return state; } };

Slide 37

Slide 37

USEREDUCER const [state, dispatch] = useReducer(reducer, defaultState); const const const const emit = useCallback((type, payload) =>” dispatch({type, payload}), []); sortBy = useCallback(sortBy =>” emit(“sort”, sortBy), []); filter = useCallback(filter =>” emit(“filter”, filter), []); onColorChange = useCallback(hex =>” emit(“change”, hex), []);

Slide 38

Slide 38

USEREDUCER const [state, dispatch] = useReducer(reducer, defaultState); const const const const emit = useCallback((type, payload) =>” dispatch({type, payload}), []); sortBy = useCallback(sortBy =>” emit(“sort”, sortBy), []); filter = useCallback(filter =>” emit(“filter”, filter), []); onColorChange = useCallback(hex =>” emit(“change”, hex), []);

Slide 39

Slide 39

USEREDUCER export default () =>” { const [state, dispatch] = useReducer(reducers, defaultState); const const const const emit = useCallback((type, payload) =>” dispatch({ type, payload }), []); sortBy = useCallback(sortBy =>” emit(“sort”, sortBy), []); filter = useCallback(filter =>” emit(“filter”, filter), []); onColorChange = useCallback(hex =>” emit(“change”, hex), []); return ( <AppUI {…state} sortBy={sortBy} filter={filter} onColorChange={onColorChange} /> ); };

Slide 40

Slide 40

USEEFFECT

Slide 41

Slide 41

USEEFFECT ACCEPTS A FUNCTION THAT CONTAINS IMPERATIVE, POSSIBLY EFFECTFUL CODE. react docs

Slide 42

Slide 42

USEEFFECT THINK OF EFFECTS AS AN ESCAPE HATCH FROM REACT’S PURELY FUNCTIONAL WORLD INTO THE IMPERATIVE WORLD. react docs

Slide 43

Slide 43

NETWORK (DATA FETCHING…), DOM OR WINDOW (TITLE UPDATES, SUBSCRIBE TO WINDOW RESIZE OR MOUSE EVENTS, ACCESS LOCAL STORAGE), LOGGING (ANALYTICS…)

Slide 44

Slide 44

FETCHING DATA useEffect(() =>” { const fetchData = async () =>” { const { data } = await axios.get(/api/colors/${props.id}); setColor(data); }; fetchData(); }, [props.id]);

Slide 45

Slide 45

FETCHING DATA NOT BEING ABLE TO USE AN ASYNC FUNCTION WAS QUITE A GOTCHA FOR ME useEffect(() =>” { const fetchData = async () =>” { const { data } = await axios.get(/api/colors/${props.id}); setColor(data); }; fetchData(); }, [props.id]);

Slide 46

Slide 46

FETCHING DATA NOT BEING ABLE TO USE AN ASYNC FUNCTION WAS QUITE A GOTCHA FOR ME useEffect(() =>” { const fetchData = async () =>” { const { data } = await axios.get(/api/colors/${props.id}); setColor(data); }; fetchData(); }, [props.id]); LIST OF DEPENDENCIES

Slide 47

Slide 47

REACT LIFECYCLES WITH HOOKS THE EMPTY DEPENDENCY LIST GUARANTEES IT’S EXECUTED ONLY ONCE NO MATTER THE NUMBER OF RE-RENDERS //componentDidMount useEffect(() =>” console.log(‘mounted’), []); //componentDidUnmount useEffect(() =>” () =>” { console.log(‘will unmount’); }, []);

Slide 48

Slide 48

REACT LIFECYCLES WITH HOOKS THE EMPTY DEPENDENCY LIST GUARANTEES IT’S EXECUTED ONLY ONCE NO MATTER THE NUMBER OF RE-RENDERS //componentDidMount useEffect(() =>” console.log(‘mounted’), []); IF THE CALLBACK RETURNS A FUNCTION, IT WILL BE CALLED BEFORE THE COMPONENT IS REMOVED FROM THE UI. //componentDidUnmount useEffect(() =>” () =>” { console.log(‘will unmount’); }, []);

Slide 49

Slide 49

REACT LIFECYCLES WITH HOOKS THE QUESTION IS NOT “WHEN DOES THIS EFFECT RUN” THE QUESTION IS “WITH WHICH STATE DOES THIS EFFECT SYNCHRONIZE WITH” ryan florence

Slide 50

Slide 50

WHAT STATE THE EFFECT SYNCS TO? useEffect(() =>” console.log(‘mounted’)); ALL STATE CHANGES useEffect(() =>” console.log(”), []); useEffect( () =>” console.log(fetch ${props.id}), [props.id] );

Slide 51

Slide 51

WHAT STATE THE EFFECT SYNCS TO? useEffect(() =>” console.log(‘mounted’)); ALL STATE CHANGES useEffect(() =>” console.log(”), []); useEffect( () =>” console.log(fetch ${props.id}), [props.id] ); NO STATE CHANGES

Slide 52

Slide 52

WHAT STATE THE EFFECT SYNCS TO? useEffect(() =>” console.log(‘mounted’)); ALL STATE CHANGES useEffect(() =>” console.log(”), []); useEffect( () =>” console.log(fetch ${props.id}), [props.id] ); PROP ID CHANGES NO STATE CHANGES

Slide 53

Slide 53

UPDATE THE URL WITH USEEFFECT

Slide 54

Slide 54

MATCH URL TO STATE const [urlRestored, setUrlRestored] = useState(false); useEffect(() =>” { if (!urlRestored) { listenToHistory(data =>” { const { currentFilter, currentSortBy } = data; // from the URL sortBy(currentSortBy || defaultSortBy); filter(currentFilter || defaultFilter); }); setUrlRestored(true); } else { objectToHistory({ currentSortBy, currentFilter, }); OUR DEPENDENCIES ARE THE VALUES } WE WANT TO PERSIST IN THE URL }, [currentSortBy, currentFilter]);

Slide 55

Slide 55

MATCH URL TO STATE const [urlRestored, setUrlRestored] = useState(false); ON FIRST LOAD WE NEED TO RESTORE THE STATE FROM THE URL AND SUBSCRIBE TO THE HISTORY EVENTS useEffect(() =>” { if (!urlRestored) { listenToHistory(data =>” { const { currentFilter, currentSortBy } = data; // from the URL sortBy(currentSortBy || defaultSortBy); filter(currentFilter || defaultFilter); }); setUrlRestored(true); } else { objectToHistory({ currentSortBy, currentFilter, }); OUR DEPENDENCIES ARE THE VALUES } WE WANT TO PERSIST IN THE URL }, [currentSortBy, currentFilter]);

Slide 56

Slide 56

MATCH URL TO STATE const [urlRestored, setUrlRestored] = useState(false); ON FIRST LOAD WE NEED TO RESTORE THE STATE FROM THE URL AND SUBSCRIBE TO THE HISTORY EVENTS useEffect(() =>” { if (!urlRestored) { listenToHistory(data =>” { const { currentFilter, currentSortBy } = data; // from the URL sortBy(currentSortBy || defaultSortBy); filter(currentFilter || defaultFilter); }); setUrlRestored(true); } else { objectToHistory({ currentSortBy, currentFilter, }); OUR DEPENDENCIES ARE THE VALUES } WE WANT TO PERSIST IN THE URL }, [currentSortBy, currentFilter]);

Slide 57

Slide 57

MATCH URL TO STATE export const listenToHistory = callback =>” { const getQs = () =>” qs.parse(window.location.search); window.addEventListener(“popstate”, () =>” callback(getQs())); callback(getQs()); }; THE CALLBACK GETS INVOKED IMMEDIATELY AND IN RESPONSE OF ANY POPSTATE EVENT WITH THE PARAMETERS IN THE QUERYSTRING listenToHistory(data =>” { const { currentFilter, currentSortBy } = data; // from the URL sortBy(currentSortBy || defaultSortBy); filter(currentFilter || defaultFilter); });

Slide 58

Slide 58

MATCH URL TO STATE export const listenToHistory = callback =>” { const getQs = () =>” qs.parse(window.location.search); window.addEventListener(“popstate”, () =>” callback(getQs())); callback(getQs()); }; THE CALLBACK GETS INVOKED IMMEDIATELY AND IN RESPONSE OF ANY POPSTATE EVENT WITH THE PARAMETERS IN THE QUERYSTRING listenToHistory(data =>” { const { currentFilter, currentSortBy } = data; // from the URL sortBy(currentSortBy || defaultSortBy); filter(currentFilter || defaultFilter); }); IN THE CALLBACK WE SET THE STATE FOR ANY CORRESPONDING PARAMETER IN THE URL

Slide 59

Slide 59

MATCH URL TO STATE const [urlRestored, setUrlRestored] = useState(false); ON FIRST LOAD WE NEED TO RESTORE THE STATE FROM THE URL AND SUBSCRIBE TO THE HISTORY EVENTS useEffect(() =>” { if (!urlRestored) { listenToHistory(data =>” { const { currentFilter, currentSortBy } = data; // from the URL sortBy(currentSortBy || defaultSortBy); filter(currentFilter || defaultFilter); }); setUrlRestored(true); } else { objectToHistory({ currentSortBy, currentFilter, }); OUR DEPENDENCIES ARE THE VALUES } WE WANT TO PERSIST IN THE URL }, [currentSortBy, currentFilter]);

Slide 60

Slide 60

MATCH URL TO STATE const [urlRestored, setUrlRestored] = useState(false); ON FIRST LOAD WE NEED TO RESTORE THE STATE FROM THE URL AND SUBSCRIBE TO THE HISTORY EVENTS useEffect(() =>” { if (!urlRestored) { listenToHistory(data =>” { const { currentFilter, currentSortBy } = data; // from the URL sortBy(currentSortBy || defaultSortBy); filter(currentFilter || defaultFilter); }); AND WE MAKE SURE IT HAPPENS setUrlRestored(true); ONLY THE FIRST TIME } else { objectToHistory({ currentSortBy, currentFilter, }); OUR DEPENDENCIES ARE THE VALUES } WE WANT TO PERSIST IN THE URL }, [currentSortBy, currentFilter]);

Slide 61

Slide 61

MATCH URL TO STATE const [urlRestored, setUrlRestored] = useState(false); ON FIRST LOAD WE NEED TO RESTORE THE STATE FROM THE URL AND SUBSCRIBE TO THE HISTORY EVENTS useEffect(() =>” { if (!urlRestored) { listenToHistory(data =>” { const { currentFilter, currentSortBy } = data; // from the URL sortBy(currentSortBy || defaultSortBy); filter(currentFilter || defaultFilter); }); AND WE MAKE SURE IT HAPPENS setUrlRestored(true); ONLY THE FIRST TIME } else { objectToHistory({ AFTER THAT SETUP, WE CAN PUSH currentSortBy, THE STATE CHANGES TO THE HISTORY currentFilter, }); OUR DEPENDENCIES ARE THE VALUES } WE WANT TO PERSIST IN THE URL }, [currentSortBy, currentFilter]);

Slide 62

Slide 62

MATCH URL TO STATE window.history.pushState({}, “”, ?${qs.stringify(qsObj)});

Slide 63

Slide 63

MATCH URL TO STATE const [urlRestored, setUrlRestored] = useState(false); useEffect(() =>” { if (!urlRestored) { listenToHistory(data =>” { const { currentFilter, currentSortBy } = data; // from the URL sortBy(currentSortBy || defaultSortBy); filter(currentFilter || defaultFilter); }); setUrlRestored(true); } else { objectToHistory({ currentSortBy, currentFilter, }); } }, [currentSortBy, currentFilter]);

Slide 64

Slide 64

LET’S SEE IT WORKING

Slide 65

Slide 65

CUSTOM HOOKS

Slide 66

Slide 66

CUSTOM HOOK useQueryString( { currentSortBy, currentFilter }, { currentSortBy: sortBy, currentFilter: filter }, { currentSortBy: defaultSortBy, currentFilter: defaultFilter } );

Slide 67

Slide 67

CUSTOM HOOK useQueryString( VALUES { currentSortBy, currentFilter }, { currentSortBy: sortBy, currentFilter: filter }, { currentSortBy: defaultSortBy, currentFilter: defaultFilter } );

Slide 68

Slide 68

CUSTOM HOOK SETTERS, MATCHING THE VALUES KEYS useQueryString( VALUES { currentSortBy, currentFilter }, { currentSortBy: sortBy, currentFilter: filter }, { currentSortBy: defaultSortBy, currentFilter: defaultFilter } );

Slide 69

Slide 69

CUSTOM HOOK SETTERS, MATCHING THE VALUES KEYS useQueryString( VALUES { currentSortBy, currentFilter }, { currentSortBy: sortBy, currentFilter: filter }, { currentSortBy: defaultSortBy, currentFilter: defaultFilter } ); DEFAULT VALUES, MATCHING THE VALUES KEYS

Slide 70

Slide 70

CUSTOM HOOK useQueryString( { currentSortBy, currentFilter }, { currentSortBy: sortBy, currentFilter: filter }, { currentSortBy: defaultSortBy, currentFilter: defaultFilter } ); useQueryString( [currentSortBy, sortBy, defaultSortBy], [currentFilter, filter, defaultFilter], ); VALUES SETTERS DEFAULTS

Slide 71

Slide 71

CUSTOM HOOK const [urlRestored, setUrlRestored] = useState(false); useEffect(() =>” { if (!urlRestored) { listenToHistory(data =>” { const { currentFilter, currentSortBy } = data; // from the URL sortBy(currentSortBy || defaultSortBy); filter(currentFilter || defaultFilter); }); setUrlRestored(true); } else { objectToHistory({ currentSortBy, currentFilter, }); } }, [currentSortBy, currentFilter]);

Slide 72

Slide 72

CUSTOM HOOK export const useQueryString = (values, setters, defaults) =>” { const [urlRestored, setUrlRestored] = useState(false); useEffect(() =>” { if (!urlRestored) { listenToHistory(data =>” { const { currentFilter, currentSortBy } = data; // from the URL sortBy(currentSortBy || defaultSortBy); filter(currentFilter || defaultFilter); }); setUrlRestored(true); } else { objectToHistory({ currentSortBy, currentFilter, }); } }, [currentSortBy, currentFilter]); };

Slide 73

Slide 73

CUSTOM HOOK export const useQueryString = (values, setters, defaults) =>” { const [urlRestored, setUrlRestored] = useState(false); useEffect(() =>” { if (!urlRestored) { listenToHistory(data =>” { const { currentFilter, currentSortBy } = data; // from the URL sortBy(currentSortBy || defaultSortBy); filter(currentFilter || defaultFilter); }); setUrlRestored(true); } else { objectToHistory({ currentSortBy, currentFilter, }); OUR DEPENDENCIES ARE THE VALUES } WE WANT TO PERSIST IN THE URL }, [currentSortBy, currentFilter]); };

Slide 74

Slide 74

CUSTOM HOOK export const useQueryString = (values, setters, defaults) =>” { const [urlRestored, setUrlRestored] = useState(false); useEffect(() =>” { if (!urlRestored) { listenToHistory(data =>” { const { currentFilter, currentSortBy } = data; // from the URL sortBy(currentSortBy || defaultSortBy); filter(currentFilter || defaultFilter); }); setUrlRestored(true); } else { objectToHistory({ currentSortBy, currentFilter, }); OUR DEPENDENCIES ARE THE VALUES } WE WANT TO PERSIST IN THE URL }, Object.values(values)); };

Slide 75

Slide 75

CUSTOM HOOK export const useQueryString = (values, setters, defaults) =>” { const [urlRestored, setUrlRestored] = useState(false); useEffect(() =>” { ON FIRST LOAD WE NEED TO RESTORE THE STATE FROM THE URL AND SUBSCRIBE if (!urlRestored) { TO THE HISTORY EVENTS listenToHistory(data =>” { const { currentFilter, currentSortBy } = data; // from the URL sortBy(currentSortBy || defaultSortBy); filter(currentFilter || defaultFilter); }); setUrlRestored(true); } else { objectToHistory({ currentSortBy, currentFilter, }); } }, Object.values(values)); };

Slide 76

Slide 76

CUSTOM HOOK export const useQueryString = (values, setters, defaults) =>” { const [urlRestored, setUrlRestored] = useState(false); useEffect(() =>” { ON FIRST LOAD WE NEED TO RESTORE THE STATE FROM THE URL AND SUBSCRIBE if (!urlRestored) { TO THE HISTORY EVENTS listenToHistory(data =>” { Object.keys(values).forEach(k =>” { const value = data[k]; // in the URL setters[k](value || defaults[k]); })}); setUrlRestored(true); } else { objectToHistory({ currentSortBy, currentFilter, }); } }, Object.values(values)); };

Slide 77

Slide 77

CUSTOM HOOK export const useQueryString = (values, setters, defaults) =>” { const [urlRestored, setUrlRestored] = useState(false); useEffect(() =>” { if (!urlRestored) { listenToHistory(data =>” { Object.keys(values).forEach(k =>” { const value = data[k]; // in the URL setters[k](value || defaults[k]); }) }); setUrlRestored(true); } else { objectToHistory({ currentSortBy, currentFilter, }); } }, Object.values(values)); };

Slide 78

Slide 78

CUSTOM HOOK export const useQueryString = (values, setters, defaults) =>” { const [urlRestored, setUrlRestored] = useState(false); useEffect(() =>” { if (!urlRestored) { listenToHistory(data =>” { Object.keys(values).forEach(k =>” { const value = data[k]; // in the URL setters[k](value || defaults[k]); }) }); setUrlRestored(true); } else { AFTER THAT SETUP, WE CAN PUSH objectToHistory({ THE STATE CHANGES TO THE HISTORY currentSortBy, currentFilter, }); } }, Object.values(values)); };

Slide 79

Slide 79

CUSTOM HOOK export const useQueryString = (values, setters, defaults) =>” { const [urlRestored, setUrlRestored] = useState(false); useEffect(() =>” { if (!urlRestored) { listenToHistory(data =>” { Object.keys(values).forEach(k =>” { const value = data[k]; // in the URL setters[k](value || defaults[k]); }) }); setUrlRestored(true); } else { AFTER THAT SETUP, WE CAN PUSH objectToHistory(values); THE STATE CHANGES TO THE HISTORY } }, Object.values(values)); };

Slide 80

Slide 80

CUSTOM HOOK export const useQueryString = (values, setters, defaults) =>” { const [urlRestored, setUrlRestored] = useState(false); useEffect(() =>” { if (!urlRestored) { listenToHistory(data =>” { Object.keys(values).forEach(k =>” { const value = data[k]; // in the URL setters[k](value || defaults[k]); }) }); setUrlRestored(true); } else { objectToHistory(values); } }, Object.values(values)); };

Slide 81

Slide 81

CUSTOM HOOK export const useQueryString = (values, setters, defaults) =>” { const [urlRestored, setUrlRestored] = useState(false); useEffect(() =>” { if (!urlRestored) { listenToHistory(data =>” { ABSTRACTION Object.keys(values).forEach(k =>” { const value = data[k]; // in the URL setters[k](value || defaults[k]); }) }); setUrlRestored(true); } else { ABSTRACTION objectToHistory(values); } ABSTRACTION }, Object.values(values)); };

Slide 82

Slide 82

LET’S MAKE SURE IT’S STILL WORKING

Slide 83

Slide 83

A STEP FORWARD

Slide 84

Slide 84

CUSTOM HOOK export const useQueryString = (values, setters, defaults) =>” { const [historyListener, setHistoryListener] = useState(null); useEffect(() =>” { WE COULD STORE THE LISTENER INSTEAD OF A BOOLEAN if (!historyListener) { const listener = subscribeToHistory(data =>” { Object.keys(values).forEach(k =>” { const value = data[k]; setters[k](value || defaults[k]); }); }); setHistoryListener(() =>” listener); } else { objectToHistory(values); } return () =>” unsubscribeFromHistory(historyListener); }, Object.values(values)); };

Slide 85

Slide 85

CUSTOM HOOK export const useQueryString = (values, setters, defaults) =>” { const [historyListener, setHistoryListener] = useState(null); useEffect(() =>” { WE COULD STORE THE LISTENER INSTEAD OF A BOOLEAN if (!historyListener) { const listener = subscribeToHistory(data =>” { HAVE THE SUBSCRIBER Object.keys(values).forEach(k =>” { RETURNING THE LISTENER const value = data[k]; setters[k](value || defaults[k]); }); }); setHistoryListener(() =>” listener); } else { objectToHistory(values); } return () =>” unsubscribeFromHistory(historyListener); }, Object.values(values)); };

Slide 86

Slide 86

CUSTOM HOOK export const useQueryString = (values, setters, defaults) =>” { const [historyListener, setHistoryListener] = useState(null); useEffect(() =>” { WE COULD STORE THE LISTENER INSTEAD OF A BOOLEAN if (!historyListener) { const listener = subscribeToHistory(data =>” { HAVE THE SUBSCRIBER Object.keys(values).forEach(k =>” { RETURNING THE LISTENER const value = data[k]; setters[k](value || defaults[k]); }); STORE THE LISTENER IN THE STATE }); setHistoryListener(() =>” listener); } else { objectToHistory(values); } return () =>” unsubscribeFromHistory(historyListener); }, Object.values(values)); };

Slide 87

Slide 87

CUSTOM HOOK export const useQueryString = (values, setters, defaults) =>” { const [historyListener, setHistoryListener] = useState(null); useEffect(() =>” { WE COULD STORE THE LISTENER INSTEAD OF A BOOLEAN if (!historyListener) { const listener = subscribeToHistory(data =>” { HAVE THE SUBSCRIBER Object.keys(values).forEach(k =>” { RETURNING THE LISTENER const value = data[k]; setters[k](value || defaults[k]); }); STORE THE LISTENER IN THE STATE }); setHistoryListener(() =>” listener); } else { RETURN A FUNCTION TO CLEAR THE EFFECT, objectToHistory(values); UNSUBSCRIBING FROM THE HISTORY EVENTS } return () =>” unsubscribeFromHistory(historyListener); }, Object.values(values)); };

Slide 88

Slide 88

CUSTOM HOOK export const useQueryString = (values, setters, defaults) =>” { const [historyListener, setHistoryListener] = useState(null); useEffect(() =>” { if (!historyListener) { const listener = subscribeToHistory(data =>” { Object.keys(values).forEach(k =>” { const value = data[k]; setters[k](value || defaults[k]); }); }); setHistoryListener(() =>” listener); } else { objectToHistory(values); } return () =>” unsubscribeFromHistory(historyListener); }, Object.values(values)); };

Slide 89

Slide 89

USEREDUCER export default () =>” { const [state, dispatch] = useReducer(reducers, defaultState); const emit = useCallback((type, payload) =>” dispatch({ type, payload }), []); const sortBy = useCallback(sortBy =>” emit(“sort”, sortBy), []); const filter = useCallback(filter =>” emit(“filter”, filter), []); const onColorChange = useCallback(hex =>” emit(“change”, hex), []); useQueryString( { currentSortBy: state.currentSortBy, currentFilter: state.currentFilter }, { currentSortBy: sortBy, currentFilter: filter }, defaultState ); return ( <AppUI {…state} sortBy={sortBy} filter={filter} onColorChange={onColorChange} /> ); };

Slide 90

Slide 90

CUSTOM HOOK export const useQueryString = (values, setters, defaults) =>” { const [historyListener, setHistoryListener] = useState(null); useEffect(() =>” { if (!historyListener) { const listener = subscribeToHistory(data =>” { Object.keys(values).forEach(k =>” { const value = data[k]; setters[k](value || defaults[k]); }); }); setHistoryListener(() =>” listener); } else { objectToHistory(values); } return () =>” unsubscribeFromHistory(historyListener); }, Object.values(values)); };

Slide 91

Slide 91

CUSTOM HOOKS TESTING

Slide 92

Slide 92

TESTING export const useQueryString = (values, setters, defaults) =>” { const [historyListener, setHistoryListener] = useState(null); useEffect(() =>” { if (!historyListener) { const listener = subscribeToHistory(data =>” { Object.keys(values).forEach(k =>” { const value = data[k]; setters[k](value || defaults[k]); }); }); setHistoryListener(() =>” listener); } else { objectToHistory(values); } return () =>” unsubscribeFromHistory(historyListener); }, Object.values(values)); };

Slide 93

Slide 93

TESTING export const useQueryString = (values, setters, defaults) =>” { const [historyListener, setHistoryListener] = useState(null); useEffect(() =>” { if (!historyListener) { const listener = subscribeToHistory(data =>” { Object.keys(values).forEach(k =>” { const value = data[k]; setters[k](value || defaults[k]); }); }); setHistoryListener(() =>” listener); } else { objectToHistory(values); } return () =>” unsubscribeFromHistory(historyListener); }, Object.values(values)); };

Slide 94

Slide 94

TESTING jest.mock(“./utils”, () =>” ({ objectToHistory: jest.fn(), subscribeToHistory: jest.fn(() =>” “listener function”), unsubscribeFromHistory: jest.fn(), })); const stateSetter = jest.fn(); const Component = ({ value }) =>” { useQueryString( { value }, { value: stateSetter }, { value: “defaultValue” } ); return null; };

Slide 95

Slide 95

TESTING jest.mock(“./utils”, () =>” ({ objectToHistory: jest.fn(), subscribeToHistory: jest.fn(() =>” “listener function”), unsubscribeFromHistory: jest.fn(), })); const stateSetter = jest.fn(); const Component = ({ value }) =>” { useQueryString( { value }, { value: stateSetter }, { value: “defaultValue” } ); return null; };

Slide 96

Slide 96

TESTING test(“the first time: subscribe to history with the right callback”, () =>” { mount(<Component value=”a” />); expect(subscribeToHistory).toBeCalledTimes(1); const popStateListener = subscribeToHistory.mock.calls[0][0]; popStateListener({ value: “a” }); expect(stateSetter).toBeCalledWith(“a”); callback({ value: “” }); expect(stateSetter).toBeCalledWith(“defaultValue”); });

Slide 97

Slide 97

TESTING test(“the first time: subscribe to history with the right callback”, () =>” { mount(<Component value=”a” />); expect(subscribeToHistory).toBeCalledTimes(1); const popStateListener = subscribeToHistory.mock.calls[0][0]; popStateListener({ value: “b” }); expect(stateSetter).toBeCalledWith(“b”); callback({ value: “” }); expect(stateSetter).toBeCalledWith(“defaultValue”); });

Slide 98

Slide 98

TESTING test(“the first time: subscribe to history with the right callback”, () =>” { mount(<Component value=”a” />); expect(subscribeToHistory).toBeCalledTimes(1); const popStateListener = subscribeToHistory.mock.calls[0][0]; popStateListener({ value: “b” }); expect(stateSetter).toBeCalledWith(“b”); callback({ value: “” }); expect(stateSetter).toBeCalledWith(“defaultValue”); });

Slide 99

Slide 99

TESTING const wrapper = mount(<Component value=”a” />); test(“further executions should invoke objectToHistory”, () =>” { wrapper.setProps({ value: “b” }); expect(subscribeToHistory).toBeCalledTimes(0); expect(objectToHistory).toBeCalledTimes(1); }); test(“further executions with the same props should do nothing”, () =>” { wrapper.setProps({ value: “b” }); expect(subscribeToHistory).toBeCalledTimes(0); expect(objectToHistory).toBeCalledTimes(0); }); test(“unmounting should invoke unsubscribeFromHistory”, () =>” { wrapper.unmount(); expect(unsubscribeFromHistory).toBeCalledTimes(1); expect(unsubscribeFromHistory).toBeCalledWith(“listener function”); });

Slide 100

Slide 100

TESTING const wrapper = mount(<Component value=”a” />); test(“further executions should invoke objectToHistory”, () =>” { wrapper.setProps({ value: “b” }); expect(subscribeToHistory).toBeCalledTimes(0); expect(objectToHistory).toBeCalledTimes(1); }); test(“further executions with the same props should do nothing”, () =>” { wrapper.setProps({ value: “b” }); expect(subscribeToHistory).toBeCalledTimes(0); expect(objectToHistory).toBeCalledTimes(0); }); test(“unmounting should invoke unsubscribeFromHistory”, () =>” { wrapper.unmount(); expect(unsubscribeFromHistory).toBeCalledTimes(1); expect(unsubscribeFromHistory).toBeCalledWith(“listener function”); });

Slide 101

Slide 101

TESTING const wrapper = mount(<Component value=”a” />); test(“further executions should invoke objectToHistory”, () =>” { wrapper.setProps({ value: “b” }); expect(subscribeToHistory).toBeCalledTimes(0); expect(objectToHistory).toBeCalledTimes(1); }); test(“further executions with the same props should do nothing”, () =>” { wrapper.setProps({ value: “b” }); expect(subscribeToHistory).toBeCalledTimes(0); expect(objectToHistory).toBeCalledTimes(0); }); test(“unmounting should invoke unsubscribeFromHistory”, () =>” { wrapper.unmount(); expect(unsubscribeFromHistory).toBeCalledTimes(1); expect(unsubscribeFromHistory).toBeCalledWith(“listener function”); });

Slide 102

Slide 102

FAQS

Slide 103

Slide 103

WHY HOOKS AGAIN?

Slide 104

Slide 104

WHY HOOKS AGAIN? THEY PROVIDE A BETTER MENTAL MODEL OF WHAT A COMPONENT IS.

Slide 105

Slide 105

WHY HOOKS AGAIN? WITHOUT HOOKS: 46.15KB WITH HOOKS: 45.69KB ~0.5KB REMOVING 1 CLASS

Slide 106

Slide 106

ARE THEY STABLE?

Slide 107

Slide 107

ARE THEY STABLE? YES AND NO: THE BASIC ONES PROBABLY YES, BUT THEY MIGHT EVOLVE A BIT

Slide 108

Slide 108

ARE THEY STABLE? INTENTIONALLY UNDERSPECIFYING DEPENDENCIES PASSED TO USEEFFECT/USEMEMO IS THE ANY OF REACT HOOKS. YOU THINK YOU’RE BEING CLEVER BY PASSING AN EMPTY ARRAY, BUT YOU’RE PROBABLY WRONG. INTENTIONALLY UNDERSPECIFYING DEPENDENCIES PASSED TO USEEFFECT/USEMEMO IS THE ANY OF REACT HOOKS. YOU THINK YOU’RE BEING CLEVER BY PASSING AN EMPTY ARRAY, BUT YOU’RE PROBABLY WRONG.

Slide 109

Slide 109

ARE THEY STABLE? INTENTIONALLY UNDERSPECIFYING DEPENDENCIES PASSED TO USEEFFECT/USEMEMO IS THE ANY OF REACT HOOKS. YOU THINK YOU’RE BEING CLEVER BY PASSING AN EMPTY ARRAY, BUT YOU’RE PROBABLY WRONG. INTENTIONALLY UNDERSPECIFYING DEPENDENCIES PASSED TO USEEFFECT/USEMEMO IS THE ANY OF REACT HOOKS. YOU THINK YOU’RE BEING CLEVER BY PASSING AN EMPTY ARRAY, BUT YOU’RE PROBABLY WRONG.

Slide 110

Slide 110

ARE THERE MORE HOOKS?

Slide 111

Slide 111

ARE THERE MORE HOOKS? YES! A FEW MORE COME WITH REACT & CUSTOM ONES, COMMUNITY DRIVEN

Slide 112

Slide 112

SHOULD I START USING THEM NOW?

Slide 113

Slide 113

SHOULD I START USING THEM NOW? UP TO YOU, BUT THEY ARE AVAILABLE TO USE SINCE REACT V16.8.0

Slide 114

Slide 114

ARE CLASSES DISAPPEARING?

Slide 115

Slide 115

ARE CLASSES DISAPPEARING? NOT IN THE FORESEEABLE FUTURE: THE REACT TEAM WAS CLEAR ABOUT IT

Slide 116

Slide 116

WHAT’S THE MOST IMPORTANT TAKE AWAY?

Slide 117

Slide 117

WHAT’S THE MOST IMPORTANT TAKE AWAY?

Slide 118

Slide 118

colours.dsgn.it github.com/cedmax/colours reactjs.com official documentation overreacted.io dan abramov’s blog marco@cedmax.com http://cedmax.com @cedmax hooks.guide collection of React hooks usehooks.com code examples