Upgrade to Pro — share decks privately, control downloads, hide ads and more …

React refactoring, off the hook(s)!

React refactoring, off the hook(s)!

React Hooks are going to change the way we write React components and apps. Let’s explore together refactoring of a real world app with hooks, introducing the concept and exploring how the mental model of components has to evolve breaking the dichotomy of stateful class vs stateless “functional” components. We are going to deep dive in the code of the app, a quite busy stateful component, looking at how we could use hooks to refactor it.

3ca63d4e2f2be0ef47b841e63b564d18?s=128

Marco Cedaro

May 10, 2019
Tweet

Transcript

  1. OFF THE HOOK REFACTORING S

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

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

    docs A JAVASCRIPT LIBRARY FOR BUILDING USER INTERFACES
  4. react docs A JAVASCRIPT LIBRARY FOR BUILDING USER INTERFACES 1.

    DECLARATIVE, 2. COMPONENT-BASED, 3. LEARN ONCE, WRITE EVERYWHERE
  5. STATE PROPS https://thenounproject.com/indygo/collection/hand-drawn-arrows-4/

  6. Type a quote here. A SIMPLE FUNCTION COMPONENT const HelloMessage

    = (props) => ( <div> Hello {props.name} </div> ); ReactDOM.render( <HelloMessage name="Taylor" />, document.getElementById('hello-example') );
  7. Type a quote here. A SIMPLE CLASS COMPONENT class HelloMessage

    extends React.Component { render() { return ( <div> Hello {this.props.name} </div> ); } } ReactDOM.render( <HelloMessage name="Taylor" />, document.getElementById('hello-example') );
  8. NEITHER MODEL REALLY CAPTURES REACT.

  9. https://dan.church

  10. dan abramov WHAT'S A COMPONENT WHY ARE THESE MODELS INSUFFICIENT

    TO DESCRIBE REACT?
  11. dan abramov WHAT'S A COMPONENT “PURE FUNCTION” MODEL DOESN’T DESCRIBE

    LOCAL STATE WHICH IS AN ESSENTIAL REACT FEATURE.
  12. dan abramov WHAT'S A COMPONENT “CLASS” MODEL DOESN’T EXPLAIN PURE-ISH

    RENDER, DISAWOVING INHERITANCE, LACK OF DIRECT INSTANTIATION, AND “RECEIVING” PROPS.
  13. @cedmax EMAIL: NAME marco WEBSITE cedmax.com TWITTER PRONOUNS: HE/HIM

  14. WE'RE ARE GOING TO TALK ABOUT COLOURS

  15. None
  16. None
  17. None
  18. None
  19. Type a quote here. export default class App extends Component

    { state = { colors: originalList, currentFilter: "", currentSortBy: "name", style: { ...constants, color: "black", background: "white" }, }; sortBy = sortBy => { this.setState({ currentSortBy: sortBy, colors: sorters[sortBy](this.state.colors), }); }; filter = currentFilter=> { const colors = getFilteredColours(originalList, currentFilter); this.setState({ currentFilter, colors }); }; COMPONENT STRUCTURE
  20. Type a quote here. onColorChange = hex => { this.setState({

    style: { ...this.state.style, background: hex, color: getMostReadable(hex), }, }); }; return ( <AppUI {...state} sortBy={sortBy} filter={filter} onColorChange={onColorChange} /> ); }; COMPONENT STRUCTURE
  21. LET'S CODE

  22. HOOKS

  23. 1. OPT-IN 2. 100% BACKWARDS-COMPATIBLE 3. AVAILABLE NOW 
 HOOKS

    WERE RELEASED WITH REACT V16.8.0. react docs A JAVASCRIPT LIBRARY FOR BUILDING USER INTERFACES
  24. USESTATE

  25. const [colors, setColors] = useState(originalList); USESTATE ARRAY DEFAULT VALUE VALUE

    FUNCTION TO SET THE STATE
  26. const [colors, setColors] = useState(originalList); setColors(sortingFunction); CAN TAKE A FUNCTION...

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

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

    THE SAME ARRAY DEFAULT VALUE VALUE FUNCTION TO SET THE STATE CAN TAKE A FUNCTION... ... AND SO DOES USE STATE USESTATE
  29. setCurrentFilter(currentFilter); setColors(colors); COMPONENTS UPDATES GET ENQUEUED USESTATE

  30. USECALLBACK

  31. const sortBy = useCallback(e => { const { value: sortBy

    } = e.target; const sortingFunction = sorters[sortBy]; setColors(sortingFunction); }, []); USECALLBACK WRAPS A FUNCTION RETURNING
 A MEMOIZED VERSION
  32. USEREDUCER

  33. 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); }, []); STILL QUITE BUSY
  34. const onColorChange = useCallback(hex => { setStyle({ ...style, background: hex,

    color: getMostReadable(hex), }); }, []); return ( <AppUI {...state} sortBy={sortBy} filter={filter} onColorChange={onColorChange} /> ); }; STILL QUITE BUSY
  35. const [state, dispatch] = useReducer(reducer, defaultState); USEREDUCER ARRAY DEFAULT VALUE

    VALUE REDUCERS
  36. 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: sorters[payload](state.colors), }; case "change": return { ...state, style: { ...state.style, background: payload, color: getMostReadable(payload), }, }; default: return state; } }; USEREDUCER
  37. const [state, dispatch] = useReducer(reducer, 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), []); USEREDUCER
  38. const [state, dispatch] = useReducer(reducer, 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), []); USEREDUCER
  39. 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), []); return ( <AppUI {...state} sortBy={sortBy} filter={filter} onColorChange={onColorChange} /> ); }; USEREDUCER
  40. USEEFFECT

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

    docs USEEFFECT
  42. THINK OF EFFECTS AS AN ESCAPE HATCH FROM REACT’S PURELY

    FUNCTIONAL WORLD INTO THE IMPERATIVE WORLD. react docs USEEFFECT
  43. NETWORK (DATA FETCHING...), DOM OR WINDOW (TITLE UPDATES, SUBSCRIBE TO

    WINDOW RESIZE OR MOUSE EVENTS, ACCESS LOCAL STORAGE), LOGGING (ANALYTICS...)
  44. FETCHING DATA useEffect(() => { const fetchData = async ()

    => { const { data } = await axios.get(`/api/colors/${props.id}`); setColor(data); }; fetchData(); }, [props.id]);
  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]);
  46. FETCHING DATA NOT BEING ABLE TO USE AN ASYNC FUNCTION

    WAS QUITE A GOTCHA FOR ME LIST OF DEPENDENCIES useEffect(() => { const fetchData = async () => { const { data } = await axios.get(`/api/colors/${props.id}`); setColor(data); }; fetchData(); }, [props.id]);
  47. REACT LIFECYCLES WITH HOOKS //componentDidMount useEffect(() => console.log('mounted'), []); //componentDidUnmount

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

    { console.log('will unmount'); }, []); THE EMPTY DEPENDENCY LIST
 GUARANTEES IT'S EXECUTED ONLY ONCE NO MATTER THE NUMBER OF RE-RENDERS IF THE CALLBACK RETURNS A
 FUNCTION, IT WILL BE CALLED BEFORE THE COMPONENT IS REMOVED FROM THE UI. REACT LIFECYCLES WITH HOOKS
  49. THE QUESTION IS NOT "WHEN DOES THIS EFFECT RUN" 


    
 THE QUESTION IS "WITH WHICH STATE DOES THIS EFFECT SYNCHRONIZE WITH" ryan florence REACT LIFECYCLES WITH HOOKS
  50. useEffect(() => console.log('mounted')); useEffect(() => console.log(''), []); useEffect( () =>

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

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

    console.log(`fetch ${props.id}`), [props.id] ); PROP ID CHANGES WHAT STATE THE EFFECT SYNCS TO? NO STATE CHANGES ALL STATE CHANGES
  53. UPDATE THE URL WITH USEEFFECT

  54. 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]); OUR DEPENDENCIES ARE THE VALUES
 WE WANT TO PERSIST IN THE URL MATCH URL TO STATE
  55. 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]); ON FIRST LOAD WE NEED TO RESTORE
 THE STATE FROM THE URL AND SUBSCRIBE 
 TO THE HISTORY EVENTS OUR DEPENDENCIES ARE THE VALUES
 WE WANT TO PERSIST IN THE URL MATCH URL TO STATE
  56. 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]); OUR DEPENDENCIES ARE THE VALUES
 WE WANT TO PERSIST IN THE URL ON FIRST LOAD WE NEED TO RESTORE
 THE STATE FROM THE URL AND SUBSCRIBE 
 TO THE HISTORY EVENTS MATCH URL TO STATE
  57. export const listenToHistory = callback => { const getQs =

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

    () => qs.parse(window.location.search); window.addEventListener("popstate", () => callback(getQs())); callback(getQs()); }; 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 THE CALLBACK GETS INVOKED IMMEDIATELY AND IN RESPONSE OF ANY POPSTATE EVENT WITH THE PARAMETERS IN THE QUERYSTRING MATCH URL TO STATE
  59. 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]); OUR DEPENDENCIES ARE THE VALUES
 WE WANT TO PERSIST IN THE URL ON FIRST LOAD WE NEED TO RESTORE
 THE STATE FROM THE URL AND SUBSCRIBE 
 TO THE HISTORY EVENTS MATCH URL TO STATE
  60. ON FIRST LOAD WE NEED TO RESTORE
 THE STATE FROM

    THE URL AND SUBSCRIBE 
 TO THE HISTORY EVENTS 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]); OUR DEPENDENCIES ARE THE VALUES
 WE WANT TO PERSIST IN THE URL MATCH URL TO STATE AND WE MAKE SURE IT HAPPENS 
 ONLY THE FIRST TIME
  61. ON FIRST LOAD WE NEED TO RESTORE
 THE STATE FROM

    THE URL AND SUBSCRIBE 
 TO THE HISTORY EVENTS 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]); OUR DEPENDENCIES ARE THE VALUES
 WE WANT TO PERSIST IN THE URL MATCH URL TO STATE AND WE MAKE SURE IT HAPPENS 
 ONLY THE FIRST TIME AFTER THAT SETUP, WE CAN PUSH THE STATE CHANGES TO THE HISTORY
  62. window.history.pushState({}, "", `?${qs.stringify(qsObj)}`); MATCH URL TO STATE

  63. 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]); MATCH URL TO STATE
  64. LET'S SEE IT WORKING

  65. CUSTOM HOOKS

  66. CUSTOM HOOK useQueryString( { currentSortBy, currentFilter }, { currentSortBy: sortBy,

    currentFilter: filter }, { currentSortBy: defaultSortBy, currentFilter: defaultFilter } );
  67. useQueryString( { currentSortBy, currentFilter }, { currentSortBy: sortBy, currentFilter: filter

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

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

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

    CUSTOM HOOK useQueryString( { currentSortBy, currentFilter }, { currentSortBy: sortBy, currentFilter: filter }, { currentSortBy: defaultSortBy, currentFilter: defaultFilter } ); DEFAULTS
  71. 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]); CUSTOM HOOK
  72. 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]); }; CUSTOM HOOK
  73. 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]); }; OUR DEPENDENCIES ARE THE VALUES
 WE WANT TO PERSIST IN THE URL CUSTOM HOOK
  74. 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, }); } }, Object.values(values)); }; OUR DEPENDENCIES ARE THE VALUES
 WE WANT TO PERSIST IN THE URL CUSTOM HOOK
  75. 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, }); } }, Object.values(values)); }; ON FIRST LOAD WE NEED TO RESTORE
 THE STATE FROM THE URL AND SUBSCRIBE 
 TO THE HISTORY EVENTS CUSTOM HOOK
  76. 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)); }; ON FIRST LOAD WE NEED TO RESTORE
 THE STATE FROM THE URL AND SUBSCRIBE 
 TO THE HISTORY EVENTS CUSTOM HOOK
  77. 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)); }; CUSTOM HOOK
  78. 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)); }; CUSTOM HOOK AFTER THAT SETUP, WE CAN PUSH THE STATE CHANGES TO THE HISTORY
  79. 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)); }; CUSTOM HOOK AFTER THAT SETUP, WE CAN PUSH THE STATE CHANGES TO THE HISTORY
  80. 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)); }; CUSTOM HOOK
  81. 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)); }; ABSTRACTION ABSTRACTION ABSTRACTION CUSTOM HOOK
  82. LET'S MAKE SURE IT'S STILL WORKING

  83. A STEP FORWARD

  84. 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)); }; CUSTOM HOOK WE COULD STORE THE LISTENER
 INSTEAD OF A BOOLEAN
  85. 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)); }; CUSTOM HOOK HAVE THE SUBSCRIBER RETURNING THE LISTENER WE COULD STORE THE LISTENER
 INSTEAD OF A BOOLEAN
  86. 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)); }; CUSTOM HOOK STORE THE LISTENER IN THE STATE WE COULD STORE THE LISTENER
 INSTEAD OF A BOOLEAN HAVE THE SUBSCRIBER RETURNING THE LISTENER
  87. 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)); }; CUSTOM HOOK STORE THE LISTENER IN THE STATE WE COULD STORE THE LISTENER
 INSTEAD OF A BOOLEAN RETURN A FUNCTION TO CLEAR THE EFFECT, UNSUBSCRIBING FROM THE HISTORY EVENTS HAVE THE SUBSCRIBER RETURNING THE LISTENER
  88. 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)); }; CUSTOM HOOK
  89. 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} /> ); }; USEREDUCER
  90. 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)); }; CUSTOM HOOK
  91. CUSTOM HOOKS
 TESTING

  92. 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)); }; TESTING
  93. 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)); }; TESTING
  94. 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; }; TESTING
  95. 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; }; TESTING
  96. 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"); }); TESTING
  97. 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"); }); TESTING
  98. 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"); }); TESTING
  99. 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"); }); TESTING
  100. 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"); }); TESTING
  101. 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"); }); TESTING
  102. FAQS

  103. WHY HOOKS AGAIN?

  104. THEY PROVIDE A BETTER MENTAL MODEL OF WHAT A COMPONENT

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

    WHY HOOKS AGAIN?
  106. ARE THEY STABLE?

  107. YES AND NO: THE BASIC ONES PROBABLY YES, BUT THEY

    MIGHT EVOLVE A BIT ARE THEY STABLE?
  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.
  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.
  110. ARE THERE MORE HOOKS?

  111. YES! A FEW MORE COME WITH REACT & CUSTOM ONES,

    COMMUNITY DRIVEN ARE THERE MORE HOOKS?
  112. SHOULD I START USING THEM NOW?

  113. UP TO YOU, BUT THEY ARE AVAILABLE TO USE SINCE

    REACT V16.8.0 SHOULD I START USING THEM NOW?
  114. ARE CLASSES DISAPPEARING?

  115. NOT IN THE FORESEEABLE FUTURE: THE REACT TEAM WAS CLEAR

    ABOUT IT ARE CLASSES DISAPPEARING?
  116. WHAT'S THE MOST IMPORTANT TAKE AWAY?

  117. WHAT'S THE MOST IMPORTANT TAKE AWAY?

  118. colours.dsgn.it
 github.com/cedmax/colours overreacted.io
 dan abramov's blog usehooks.com
 code examples hooks.guide


    collection of React hooks reactjs.com
 official documentation marco@cedmax.com http://cedmax.com @cedmax