$30 off During Our Annual Pro Sale. View Details »

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.

Marco Cedaro

May 10, 2019
Tweet

More Decks by Marco Cedaro

Other Decks in Programming

Transcript

  1. OFF THE HOOK
    REFACTORING
    S

    View Slide

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

    View Slide

  3. 1. DECLARATIVE,
    2. COMPONENT-BASED,
    3. LEARN ONCE, WRITE EVERYWHERE
    react docs
    A JAVASCRIPT LIBRARY FOR BUILDING USER INTERFACES

    View Slide

  4. react docs
    A JAVASCRIPT LIBRARY FOR BUILDING USER INTERFACES
    1. DECLARATIVE,
    2. COMPONENT-BASED,
    3. LEARN ONCE, WRITE EVERYWHERE

    View Slide

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

    View Slide

  6. Type a quote here.
    A SIMPLE FUNCTION COMPONENT
    const HelloMessage = (props) => (

    Hello {props.name}

    );
    ReactDOM.render(
    ,
    document.getElementById('hello-example')
    );

    View Slide

  7. Type a quote here.
    A SIMPLE CLASS COMPONENT
    class HelloMessage extends React.Component {
    render() {
    return (

    Hello {this.props.name}

    );
    }
    }
    ReactDOM.render(
    ,
    document.getElementById('hello-example')
    );

    View Slide

  8. NEITHER MODEL REALLY
    CAPTURES REACT.

    View Slide

  9. https://dan.church

    View Slide

  10. dan abramov
    WHAT'S A COMPONENT
    WHY ARE THESE MODELS
    INSUFFICIENT TO DESCRIBE REACT?

    View Slide

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

    View Slide

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

    View Slide

  13. @cedmax
    EMAIL:
    NAME
    marco
    WEBSITE
    cedmax.com
    TWITTER
    PRONOUNS: HE/HIM




    View Slide

  14. WE'RE ARE GOING TO TALK ABOUT
    COLOURS

    View Slide

  15. View Slide

  16. View Slide

  17. View Slide

  18. View Slide

  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

    View Slide

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

    View Slide

  21. LET'S CODE

    View Slide

  22. HOOKS

    View Slide

  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

    View Slide

  24. USESTATE

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

  30. USECALLBACK

    View Slide

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

    A MEMOIZED VERSION

    View Slide

  32. USEREDUCER

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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 (
    {...state}
    sortBy={sortBy}
    filter={filter}
    onColorChange={onColorChange}
    />
    );
    };
    USEREDUCER

    View Slide

  40. USEEFFECT

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View 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]);

    View Slide

  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]);

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  53. UPDATE THE URL
    WITH USEEFFECT

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  62. window.history.pushState({}, "", `?${qs.stringify(qsObj)}`);
    MATCH URL TO STATE

    View Slide

  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

    View Slide

  64. LET'S SEE IT
    WORKING

    View Slide

  65. CUSTOM HOOKS

    View Slide

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

    View Slide

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

    View Slide

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

    THE VALUES KEYS
    CUSTOM HOOK

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  82. LET'S MAKE SURE
    IT'S STILL WORKING

    View Slide

  83. A STEP
    FORWARD

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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 (
    {...state}
    sortBy={sortBy}
    filter={filter}
    onColorChange={onColorChange}
    />
    );
    };
    USEREDUCER

    View Slide

  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

    View Slide

  91. CUSTOM HOOKS

    TESTING

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  96. test("the first time: subscribe to history with the right callback", () => {
    mount();
    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

    View Slide

  97. test("the first time: subscribe to history with the right callback", () => {
    mount();
    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

    View Slide

  98. test("the first time: subscribe to history with the right callback", () => {
    mount();
    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

    View Slide

  99. const wrapper = mount();
    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

    View Slide

  100. const wrapper = mount();
    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

    View Slide

  101. const wrapper = mount();
    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

    View Slide

  102. FAQS

    View Slide

  103. WHY HOOKS
    AGAIN?

    View Slide

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

    View Slide

  105. WITHOUT HOOKS: 46.15KB

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

    View Slide

  106. ARE THEY
    STABLE?

    View Slide

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

    View 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.

    View 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.

    View Slide

  110. ARE THERE MORE
    HOOKS?

    View Slide

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

    View Slide

  112. SHOULD I START
    USING THEM NOW?

    View Slide

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

    View Slide

  114. ARE CLASSES
    DISAPPEARING?

    View Slide

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

    View Slide

  116. WHAT'S THE MOST
    IMPORTANT TAKE AWAY?

    View Slide

  117. WHAT'S THE MOST
    IMPORTANT TAKE AWAY?

    View Slide

  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
    [email protected]
    http://cedmax.com
    @cedmax

    View Slide