REFACTORING OFF THE HOOKS
A presentation at jsDay in May 2019 in Verona, VR, Italy by Marco Cedaro
REFACTORING OFF THE HOOKS
YOU MIGHT HAVE HEARD OF REACT https://evilmartians.com/chronicles/optimizing-react-virtual-dom-explained
A JAVASCRIPT LIBRARY FOR BUILDING USER INTERFACES
A JAVASCRIPT LIBRARY FOR BUILDING USER INTERFACES
PROPS STATE https://thenounproject.com/indygo/collection/hand-drawn-arrows-4/
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’) );
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’) );
NEITHER MODEL REALLY CAPTURES REACT.
https://dan.church
WHAT’S A COMPONENT WHY ARE THESE MODELS INSUFFICIENT TO DESCRIBE REACT? dan abramov
WHAT’S A COMPONENT “PURE FUNCTION” MODEL DOESN’T DESCRIBE LOCAL STATE WHICH IS AN ESSENTIAL REACT FEATURE. dan abramov
WHAT’S A COMPONENT “CLASS” MODEL DOESN’T EXPLAIN PURE-ISH RENDER, DISAWOVING INHERITANCE, LACK OF DIRECT INSTANTIATION, AND “RECEIVING” PROPS. dan abramov
🤦 😬 TWITTER EMAIL: marco@cedmax cedmax.com NAME WEBSITE PRONOUNS: HE/HIM 🤷 😅
WE’RE ARE GOING TO TALK ABOUT COLOURS
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 }); };
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} /> ); };
LET’S CODE
HOOKS
A JAVASCRIPT LIBRARY FOR BUILDING USER INTERFACES
USESTATE
USESTATE DEFAULT VALUE VALUE const [colors, setColors] = useState(originalList); ARRAY FUNCTION TO SET THE STATE
USESTATE DEFAULT VALUE VALUE const [colors, setColors] = useState(originalList); ARRAY FUNCTION TO SET THE STATE CAN TAKE A FUNCTION… setColors(sortingFunction);
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
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
USESTATE setCurrentFilter(currentFilter); setColors(colors); COMPONENTS UPDATES GET ENQUEUED
USECALLBACK
USECALLBACK WRAPS A FUNCTION RETURNING A MEMOIZED VERSION const sortBy = useCallback(e =>” { const { value: sortBy } = e.target; const sortingFunction = sorters[sortBy]; setColors(sortingFunction); }, []);
USEREDUCER
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); }, []);
STILL QUITE BUSY const onColorChange = useCallback(hex =>” { setStyle({ …style, background: hex, color: getMostReadable(hex), }); }, []); return ( <AppUI {…state} sortBy={sortBy} filter={filter} onColorChange={onColorChange} /> ); };
USEREDUCER VALUE DEFAULT VALUE const [state, dispatch] = useReducer(reducer, defaultState); ARRAY REDUCERS
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; } };
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), []);
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), []);
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} /> ); };
USEEFFECT
USEEFFECT ACCEPTS A FUNCTION THAT CONTAINS IMPERATIVE, POSSIBLY EFFECTFUL CODE. react docs
USEEFFECT THINK OF EFFECTS AS AN ESCAPE HATCH FROM REACT’S PURELY FUNCTIONAL WORLD INTO THE IMPERATIVE WORLD. react docs
NETWORK (DATA FETCHING…), DOM OR WINDOW (TITLE UPDATES, SUBSCRIBE TO WINDOW RESIZE OR MOUSE EVENTS, ACCESS LOCAL STORAGE), LOGGING (ANALYTICS…)
FETCHING DATA
useEffect(() =>” { const fetchData = async () =>” { const { data } = await axios.get(/api/colors/${props.id}
); setColor(data); }; fetchData(); }, [props.id]);
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]);
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
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’); }, []);
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’); }, []);
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
WHAT STATE THE EFFECT SYNCS TO?
useEffect(() =>” console.log(‘mounted’)); ALL STATE CHANGES
useEffect(() =>” console.log(”), []); useEffect( () =>” console.log(fetch ${props.id}
), [props.id] );
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
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
UPDATE THE URL WITH USEEFFECT
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]);
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]);
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]);
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); });
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
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]);
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]);
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]);
MATCH URL TO STATE
window.history.pushState({}, “”, ?${qs.stringify(qsObj)}
);
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]);
LET’S SEE IT WORKING
CUSTOM HOOKS
CUSTOM HOOK useQueryString( { currentSortBy, currentFilter }, { currentSortBy: sortBy, currentFilter: filter }, { currentSortBy: defaultSortBy, currentFilter: defaultFilter } );
CUSTOM HOOK useQueryString( VALUES { currentSortBy, currentFilter }, { currentSortBy: sortBy, currentFilter: filter }, { currentSortBy: defaultSortBy, currentFilter: defaultFilter } );
CUSTOM HOOK SETTERS, MATCHING THE VALUES KEYS useQueryString( VALUES { currentSortBy, currentFilter }, { currentSortBy: sortBy, currentFilter: filter }, { currentSortBy: defaultSortBy, currentFilter: defaultFilter } );
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
CUSTOM HOOK useQueryString( { currentSortBy, currentFilter }, { currentSortBy: sortBy, currentFilter: filter }, { currentSortBy: defaultSortBy, currentFilter: defaultFilter } ); useQueryString( [currentSortBy, sortBy, defaultSortBy], [currentFilter, filter, defaultFilter], ); VALUES SETTERS DEFAULTS
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]);
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]); };
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]); };
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)); };
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)); };
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)); };
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)); };
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)); };
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)); };
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)); };
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)); };
LET’S MAKE SURE IT’S STILL WORKING
A STEP FORWARD
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)); };
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)); };
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)); };
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)); };
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)); };
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} /> ); };
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)); };
CUSTOM HOOKS TESTING
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)); };
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)); };
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; };
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; };
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”); });
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”); });
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”); });
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”); });
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”); });
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”); });
FAQS
WHY HOOKS AGAIN?
WHY HOOKS AGAIN? THEY PROVIDE A BETTER MENTAL MODEL OF WHAT A COMPONENT IS.
WHY HOOKS AGAIN? WITHOUT HOOKS: 46.15KB WITH HOOKS: 45.69KB ~0.5KB REMOVING 1 CLASS
ARE THEY STABLE?
ARE THEY STABLE? YES AND NO: THE BASIC ONES PROBABLY YES, BUT THEY MIGHT EVOLVE A BIT
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.
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.
ARE THERE MORE HOOKS?
ARE THERE MORE HOOKS? YES! A FEW MORE COME WITH REACT & CUSTOM ONES, COMMUNITY DRIVEN
SHOULD I START USING THEM NOW?
SHOULD I START USING THEM NOW? UP TO YOU, BUT THEY ARE AVAILABLE TO USE SINCE REACT V16.8.0
ARE CLASSES DISAPPEARING?
ARE CLASSES DISAPPEARING? NOT IN THE FORESEEABLE FUTURE: THE REACT TEAM WAS CLEAR ABOUT IT
WHAT’S THE MOST IMPORTANT TAKE AWAY?
WHAT’S THE MOST IMPORTANT TAKE AWAY?
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