From 89ee435c84c15aca88252367a10a2d71228eb283 Mon Sep 17 00:00:00 2001 From: alexlbr Date: Wed, 15 Apr 2020 22:34:37 +0100 Subject: [PATCH 1/9] add solution context part 2 --- .../Context/exercise/GraphQLProvider.jsx | 49 ++++++++++++++----- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/src/components/patterns/Context/exercise/GraphQLProvider.jsx b/src/components/patterns/Context/exercise/GraphQLProvider.jsx index 3a32bcf..b10e4c2 100644 --- a/src/components/patterns/Context/exercise/GraphQLProvider.jsx +++ b/src/components/patterns/Context/exercise/GraphQLProvider.jsx @@ -1,11 +1,13 @@ import React, { useReducer, useContext, useEffect } from "react"; -import { memoize, hashGql, createClient } from "./utils"; +import { memoize, hashGql } from "./utils"; const RECEIVE_DATA = "RECEIVE_DATA"; const SET_ERROR = "SET_ERROR"; export const StoreContext = React.createContext(); -// 🚧 1.1 Create a context for the data fetching client +export const CacheContext = React.createContext(); +export const DispatchGQLContext = React.createContext(); +export const ClientContext = React.createContext(); const reducer = (state, action) => { switch (action.type) { @@ -25,6 +27,7 @@ const reducer = (state, action) => { export const GraphQLProvider = ({ children, + client, initialState = { data: {}, error: null, @@ -33,22 +36,24 @@ export const GraphQLProvider = ({ }) => { const [state, dispatch] = useReducer(reducer, initialState); - // 🚧 Part 1.2. Add your ClientProvider inside the return return ( - - {children} - + + + + {children} + + + ); }; -// 🚧 Bonus exercise, should we use useMemo for this memoized function? Why? +// 🚧 Should we use useMemo for this memoized function? Why? const memoizedHashGql = memoize(hashGql); export const useQuery = (query, { variables }) => { - // 🚧 1.3. Use the client from the context, instead of this hardcoded implementation. You can create a handy useClient custom hook (almost implemented at the end of the file). - // Why moving the client to the context? For testing. E.g. https://www.apollographql.com/docs/react/development-testing/testing/#mockedprovider - const client = createClient({ url: "https://rickandmortyapi.com/graphql/" }); - const [state, dispatch] = useContext(StoreContext); + const { client } = useClient(); + const { state } = useGQLCache(CacheContext); + const { dispatch } = useGQLDispatch(); const { loading, error, data: cache } = state; const cacheKey = memoizedHashGql(query, variables); const data = cache && cache[cacheKey]; @@ -72,13 +77,31 @@ export const useQuery = (query, { variables }) => { error, }) ); - }, [query, cacheKey, variables, dispatch, data]); + }, [query, cacheKey, variables, dispatch, data]); // do I need dispatch here if it comes from useReducer? return { data, loading, error }; }; +export const useGQLDispatch = () => { + const { dispatch } = useContext(DispatchGQLContext) || {}; + if (!dispatch) { + throw new Error("No DispatchGQLContext found!"); + } + + return { dispatch }; +}; + +export const useGQLCache = () => { + const { state } = useContext(CacheContext) || {}; + if (!state) { + throw new Error("No CacheContext found!"); + } + + return { state }; +}; + export const useClient = () => { - const client = null; // 🚧 get the client from the context here + const { client } = useContext(ClientContext) || {}; if (!client) { throw new Error( "No GraphQL client found, please make sure that you are providing a client prop to the GraphQL Provider" From 4898e3d3a9e2096f14fde90931e41457d8edb551 Mon Sep 17 00:00:00 2001 From: alexlbr Date: Wed, 15 Apr 2020 23:33:50 +0100 Subject: [PATCH 2/9] remove questions folder --- src/components/questions/Question6/index.js | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 src/components/questions/Question6/index.js diff --git a/src/components/questions/Question6/index.js b/src/components/questions/Question6/index.js deleted file mode 100644 index 8b6c58d..0000000 --- a/src/components/questions/Question6/index.js +++ /dev/null @@ -1,16 +0,0 @@ -import React from 'react' -import Example from '../../patterns/StyledVariants/example' -import Exercise from '../../patterns/StyledVariants/exercise' - -const Question6 = (props) => ( -
-

Variants

-

Example

- -
-

Exercise

- -
-) - -export default Question6; From d9a53501fc20f957564c89884695948da9a12c36 Mon Sep 17 00:00:00 2001 From: alexlbr Date: Wed, 15 Apr 2020 23:47:07 +0100 Subject: [PATCH 3/9] implement custom hook solution --- src/components/App.jsx | 10 ++- .../CompoundComponents/exercise/Menu.jsx | 33 ++++----- .../patterns/CustomHooks/exercise/index.jsx | 13 ++-- .../patterns/CustomHooks/exercise/useWidth.js | 72 +++++++++---------- 4 files changed, 62 insertions(+), 66 deletions(-) diff --git a/src/components/App.jsx b/src/components/App.jsx index 8cb093f..d446d49 100644 --- a/src/components/App.jsx +++ b/src/components/App.jsx @@ -2,9 +2,7 @@ import React from "react"; import { Route, Redirect } from "react-router-dom"; import Root from "./Root"; -import withWidth, { - LARGE, -} from "./patterns/HigherOrderComponents/exercise_2/withWidth"; +import useWidth, { LARGE } from "./patterns/CustomHooks/exercise/useWidth"; import HigherOrderComponentsPage from "./patterns/HigherOrderComponents/Page"; import RenderPropsPage from "./patterns/RenderProps/Page"; import CompoundComponentsPage from "./patterns/CompoundComponents/Page"; @@ -143,4 +141,10 @@ class App extends React.Component { } } +const withWidth = (Component) => (props) => { + const width = useWidth(); + + return ; +}; + export default withWidth(App); diff --git a/src/components/patterns/CompoundComponents/exercise/Menu.jsx b/src/components/patterns/CompoundComponents/exercise/Menu.jsx index 3a32a90..2e7a06c 100644 --- a/src/components/patterns/CompoundComponents/exercise/Menu.jsx +++ b/src/components/patterns/CompoundComponents/exercise/Menu.jsx @@ -1,22 +1,23 @@ import React from "react"; import SideMenu from "react-burger-menu"; -import withWidth, { - LARGE -} from "../../HigherOrderComponents/exercise_2/withWidth"; import FloatingMenuBtn from "../../../FloatingMenuBtn"; +import useWidth, { LARGE } from "../../CustomHooks/exercise/useWidth"; -const Menu = ({ isOpen, children, pageWrapId, width, toggleMenu }) => ( -
- {width === LARGE ? "" : } - - {children} - -
-); +const Menu = ({ isOpen, children, pageWrapId, toggleMenu }) => { + const width = useWidth(); + return ( +
+ {width === LARGE ? "" : } + + {children} + +
+ ); +}; -export default withWidth(Menu); +export default Menu; diff --git a/src/components/patterns/CustomHooks/exercise/index.jsx b/src/components/patterns/CustomHooks/exercise/index.jsx index 0e60a3b..bc5973f 100644 --- a/src/components/patterns/CustomHooks/exercise/index.jsx +++ b/src/components/patterns/CustomHooks/exercise/index.jsx @@ -1,10 +1,10 @@ /* eslint-disable jsx-a11y/accessible-emoji */ import React from "react"; -// import useWidth, { LARGE, MEDIUM } from "./useWidth"; -// remove the following import after refactoring the Width component to a custom hook -import Width from "./useWidth"; +import useWidth from "./useWidth"; const Bonus = () => { + const width = useWidth(); + return (
@@ -14,12 +14,7 @@ const Bonus = () => { avoiding common pitfalls -

- {/* Comment out the following after implementing part 1 */} - The width is: - {/* Use the width value from your custom hook in the next line after you implment part 1 */} - {/* {width} */} -

+

{width}

Part 1, refactoring

diff --git a/src/components/patterns/CustomHooks/exercise/useWidth.js b/src/components/patterns/CustomHooks/exercise/useWidth.js index 6ac819a..83c8272 100644 --- a/src/components/patterns/CustomHooks/exercise/useWidth.js +++ b/src/components/patterns/CustomHooks/exercise/useWidth.js @@ -1,4 +1,4 @@ -import React from "react"; +import { useState, useEffect, useCallback } from "react"; export const SMALL = 1; export const MEDIUM = 2; @@ -7,49 +7,45 @@ export const LARGE = 3; const largeWidth = 992, mediumWidth = 768; -class WithWidth extends React.Component { - constructor() { - super(); - this.state = { width: this.windowWidth() }; +const windowWidth = () => { + let innerWidth = 0; + let width; + if (window) innerWidth = window.innerWidth; + + if (innerWidth >= largeWidth) { + width = LARGE; + } else if (innerWidth >= mediumWidth) { + width = MEDIUM; + } else { + width = SMALL; } - componentDidMount() { - if (typeof window !== "undefined") { - window.addEventListener("resize", this.handleResize); - this.handleResize(); - } - } + return width; +}; - componentWillUnmount() { - if (typeof window !== "undefined") - window.removeEventListener("resize", this.handleResize); - } +const useWidth = () => { + const [width, setWidth] = useState(null); - handleResize = () => { - let width = this.windowWidth(); - if (width !== this.state.width) this.setState({ width }); + const handleResize = () => { + let currentWidth = windowWidth(); + if (currentWidth !== width) { + setWidth(currentWidth); + } }; - windowWidth() { - let innerWidth = 0; - let width; - if (window) innerWidth = window.innerWidth; - - if (innerWidth >= largeWidth) { - width = LARGE; - } else if (innerWidth >= mediumWidth) { - width = MEDIUM; - } else { - // innerWidth < 768 - width = SMALL; - } + const handleResizeCallback = useCallback(handleResize, []); - return width; - } + useEffect(() => { + if (window) { + window.addEventListener("resize", handleResizeCallback); + handleResizeCallback(); + } + return () => { + if (window) window.removeEventListener("resize", handleResizeCallback); + }; + }, [handleResizeCallback]); - render() { - return this.state.width; - } -} + return width; +}; -export default WithWidth; +export default useWidth; From ae98353e7ef00a74fb41e17e9dfc583c689155fc Mon Sep 17 00:00:00 2001 From: alexlbr Date: Fri, 17 Apr 2020 17:22:56 +0100 Subject: [PATCH 4/9] update different patterns --- src/components/App.jsx | 11 +-- .../closure/exercise.js | 4 +- .../composition/Page.jsx | 44 +++++++++--- .../composition/bonus/index.js | 62 ++++++++++++++++ .../{exercise => bonus}/validators.js | 0 .../composition/example/index.js | 21 ------ .../composition/exercise/index.js | 61 ++-------------- .../memoization/exercise.js | 30 ++++---- .../Context/exercise/GraphQLProvider.jsx | 6 +- .../patterns/Context/exercise/index.jsx | 3 +- .../patterns/Context/exercise/utils.js | 2 + .../patterns/CustomHooks/exercise/useWidth.js | 28 ++++---- .../patterns/HookReducer/exercise/index.jsx | 71 ++++++++++--------- 13 files changed, 186 insertions(+), 157 deletions(-) create mode 100644 src/components/functional-programming/composition/bonus/index.js rename src/components/functional-programming/composition/{exercise => bonus}/validators.js (100%) delete mode 100644 src/components/functional-programming/composition/example/index.js diff --git a/src/components/App.jsx b/src/components/App.jsx index d446d49..613c022 100644 --- a/src/components/App.jsx +++ b/src/components/App.jsx @@ -2,7 +2,7 @@ import React from "react"; import { Route, Redirect } from "react-router-dom"; import Root from "./Root"; -import useWidth, { LARGE } from "./patterns/CustomHooks/exercise/useWidth"; +import { withWidth, LARGE } from "./patterns/CustomHooks/exercise/useWidth"; import HigherOrderComponentsPage from "./patterns/HigherOrderComponents/Page"; import RenderPropsPage from "./patterns/RenderProps/Page"; import CompoundComponentsPage from "./patterns/CompoundComponents/Page"; @@ -18,6 +18,7 @@ import ThemingPage from "./patterns/Theming/Page"; import VariantsPage from "./patterns/Variants/Page"; import HooksPage from "./patterns/Hooks/Page"; import RGALogo from "./RGALogo"; +import { compose } from "./functional-programming/composition/exercise"; class App extends React.Component { constructor() { @@ -141,10 +142,4 @@ class App extends React.Component { } } -const withWidth = (Component) => (props) => { - const width = useWidth(); - - return ; -}; - -export default withWidth(App); +export default compose(withWidth)(App); diff --git a/src/components/functional-programming/closure/exercise.js b/src/components/functional-programming/closure/exercise.js index 619d3db..98d0ea4 100644 --- a/src/components/functional-programming/closure/exercise.js +++ b/src/components/functional-programming/closure/exercise.js @@ -3,11 +3,11 @@ // Open the console on your browser and type [closure exercise] in the console filter. // You should see on the console the console.log() for this exercise. -function add() {} +const add = (x) => (y) => x + y; const addFive = add(5); let result; -//result = addFive(7); // should output 12 +result = addFive(7); // should output 12 console.log(`[closure exercise] addFive(7) is ${result}`); diff --git a/src/components/functional-programming/composition/Page.jsx b/src/components/functional-programming/composition/Page.jsx index 9a0343e..d692ac3 100644 --- a/src/components/functional-programming/composition/Page.jsx +++ b/src/components/functional-programming/composition/Page.jsx @@ -1,26 +1,48 @@ +/* eslint-disable jsx-a11y/accessible-emoji */ import React from "react"; -import { transformText } from "./example"; -import FormExercise from "./exercise"; +import { transformText } from "./exercise"; +import FormExercise from "./bonus"; const exampleText = "1 2 3 React GraphQL Academy is a m a z i n g"; const Page = () => (

Function composition

-

Example

- Tranform the following text: "{exampleText}" so it becomes - REACTJSACADEMYISAMAZING +

Exercise

+ With the transformText function we can transform{" "} + "{exampleText}" into + "{transformText(exampleText)}"

- Result: {transformText(exampleText)} + Let's make that composition more declarative using a compose{" "} + function:

-

Exercise

+

+ 1. Your first task is to implement the compose + function. Go to{" "} + + {" "} + src/components/functional-programming/composition/exercise/index.js + {" "} + and follow the hints. +

+

+ 2. Can you use your compose{" "} + function to compose HoCs? You can try to use it along with the{" "} + withWidth at the bottom of the file{" "} + src/components/App.jsx +

+

Bonus Exercise

Validate the following form composing the validators defined in - `src/components/functional-programming/composition/exercise/valiators`. To - do that you'll need to finish the implementation of the composeValidators - function defined in - `src/components/functional-programming/composition/exercise/index` + + src/components/functional-programming/composition/bonus/valiators + + . To do that you'll need to finish the implementation of the + composeValidators function defined in + + src/components/functional-programming/composition/bonus/index.js +

Notes

diff --git a/src/components/functional-programming/composition/bonus/index.js b/src/components/functional-programming/composition/bonus/index.js new file mode 100644 index 0000000..1b0b893 --- /dev/null +++ b/src/components/functional-programming/composition/bonus/index.js @@ -0,0 +1,62 @@ +import React from "react"; +import { Form, Field } from "react-final-form"; +import { required, mustBeEmail, atLeastFiveCharacters } from "./validators"; + +// Task 1, implement the composeValidators function +// each validator has a value as input and returns undefined or the error message +export const composeValidators = (...validators) => (value) => + validators.reduceRight( + (error, validator) => error || validator(value), + undefined + ); + +// Task 2, you need to use the composeValidators so +// - Email is validated with required and mustBeEmail +// - Password is validatie with required and atLeastFiveCharacters +const FormExercise = () => ( +
( + +

+ +
+ Task: validate with required and must be an email +

+

+ +
+ Task: validate with required and min length 5 characters +

+ +
+ )} + /> +); + +const onSubmit = () => {}; + +const Input = ({ input, meta, placeholder, type }) => ( + + + {meta.error && meta.touched && ( + {meta.error} + )} + +); + +export default FormExercise; diff --git a/src/components/functional-programming/composition/exercise/validators.js b/src/components/functional-programming/composition/bonus/validators.js similarity index 100% rename from src/components/functional-programming/composition/exercise/validators.js rename to src/components/functional-programming/composition/bonus/validators.js diff --git a/src/components/functional-programming/composition/example/index.js b/src/components/functional-programming/composition/example/index.js deleted file mode 100644 index 5bcb9cd..0000000 --- a/src/components/functional-programming/composition/example/index.js +++ /dev/null @@ -1,21 +0,0 @@ -/* eslint-disable no-unused-vars */ - -export const compose = (...functions) => (initialValue) => - functions.reduceRight( - (accumulatedValue, fn) => fn(accumulatedValue), - initialValue - ); - -const toUpperCase = (text) => text.toUpperCase(); - -const removeSpaces = (text) => text.replace(/\s/g, ""); - -const removeNumbers = (text) => text.replace(/[0-9]/g, ""); - -// export const transformText = compose( -// toUpperCase, -// removeNumbers, -// removeSpaces -// ); - -export const transformText = (text) => text; diff --git a/src/components/functional-programming/composition/exercise/index.js b/src/components/functional-programming/composition/exercise/index.js index e5f09f8..44d6d87 100644 --- a/src/components/functional-programming/composition/exercise/index.js +++ b/src/components/functional-programming/composition/exercise/index.js @@ -1,59 +1,12 @@ -import React from "react"; -import { Form, Field } from "react-final-form"; -import { required, mustBeEmail, atLeastFiveCharacters } from "./validators"; +/* eslint-disable no-unused-vars */ -// Task 1, implement the composeValidators function -// each validator has a value as input and returns undefined or the error message -export const composeValidators = (...validators) => value => - validators.reduceRight((error, validator) => undefined, undefined); +const toUpperCase = (text) => text.toUpperCase(); -// Task 2, you need to use the composeValidators so -// - Email is validated with required and mustBeEmail -// - Password is validatie with required and atLeastFiveCharacters -const FormExercise = () => ( -
( - -

- -
- Task: validate with required and must be an email -

-

- -
- Task: validate with required and min length 5 characters -

- -
- )} - /> -); +const removeSpaces = (text) => text.replace(/\s/g, ""); -const onSubmit = () => {}; +const removeNumbers = (text) => text.replace(/[0-9]/g, ""); -const Input = ({ input, meta, placeholder, type }) => ( - - - {meta.error && meta.touched && ( - {meta.error} - )} - -); +export const compose = (...functions) => (initialValue) => + functions.reduceRight((acc, fn) => fn(acc), initialValue); -export default FormExercise; +export const transformText = compose(toUpperCase, removeNumbers, removeSpaces); diff --git a/src/components/functional-programming/memoization/exercise.js b/src/components/functional-programming/memoization/exercise.js index 89f1d64..73b0352 100644 --- a/src/components/functional-programming/memoization/exercise.js +++ b/src/components/functional-programming/memoization/exercise.js @@ -31,25 +31,31 @@ export function doAnyWork(amount = 1, amount2 = 1, amount3 = 1) { return amount + amount2 + amount3; } -function memoize(fn) { +const resolverFn = (args) => args.join(","); + +function memoize(fn, resolver = resolverFn) { let cache = {}; - return (amount) => { - if (amount in cache) { + return (...args) => { + const key = resolver(args); + + if (key in cache) { console.log("[memoization exercise] output from cache"); - return cache[amount]; + return cache[key]; } else { - let result = fn(amount); - cache[amount] = result; + let result = fn(...args); + cache[key] = result; return result; } }; } -const memoizedDoWork = memoize(doEasyWork); -memoizedDoWork(4000); -memoizedDoWork(4000); +// const memoizedDoWork = memoize(doHardWork); +// memoizedDoWork(4000); +// memoizedDoWork(4000); // Bounus -// const memoizedDoWork = memoize(doAnyWork); -// console.log(`[memoization exercise] ${memoizedDoWork(1, 2, 3)} === 6 ?`); -// console.log(`[memoization exercise] ${memoizedDoWork(1, 50, 104)} === 155 ?`); +const memoizedDoWork = memoize(doAnyWork); +console.log(`[memoization exercise] ${memoizedDoWork(1, 2, 3)} === 6 ?`); +console.log(`[memoization exercise] ${memoizedDoWork(1, 50, 104)} === 155 ?`); + +// Bonus 2, extract the key cache functionality to a "resolver" function diff --git a/src/components/patterns/Context/exercise/GraphQLProvider.jsx b/src/components/patterns/Context/exercise/GraphQLProvider.jsx index b10e4c2..b31b02c 100644 --- a/src/components/patterns/Context/exercise/GraphQLProvider.jsx +++ b/src/components/patterns/Context/exercise/GraphQLProvider.jsx @@ -38,6 +38,7 @@ export const GraphQLProvider = ({ return ( + {/* Anwser to task 2: It doesn't make sense in this case to split it in the following two contexts */} {children} @@ -47,7 +48,6 @@ export const GraphQLProvider = ({ ); }; -// 🚧 Should we use useMemo for this memoized function? Why? const memoizedHashGql = memoize(hashGql); export const useQuery = (query, { variables }) => { @@ -59,7 +59,7 @@ export const useQuery = (query, { variables }) => { const data = cache && cache[cacheKey]; useEffect(() => { - if (data) { + if (data || error) { return; } @@ -77,7 +77,7 @@ export const useQuery = (query, { variables }) => { error, }) ); - }, [query, cacheKey, variables, dispatch, data]); // do I need dispatch here if it comes from useReducer? + }, [query, cacheKey, variables, dispatch, data]); return { data, loading, error }; }; diff --git a/src/components/patterns/Context/exercise/index.jsx b/src/components/patterns/Context/exercise/index.jsx index 622c1ad..764b1a5 100644 --- a/src/components/patterns/Context/exercise/index.jsx +++ b/src/components/patterns/Context/exercise/index.jsx @@ -2,6 +2,7 @@ import React from "react"; import { useQuery } from "./GraphQLProvider"; +const variables = { id: 2 }; const Root = () => { const { data, loading, error } = useQuery( `query character($id: ID! = 1) { @@ -10,7 +11,7 @@ const Root = () => { name } }`, - { variables: { id: 2 } } + { variables } ); if (loading) { return "loading"; diff --git a/src/components/patterns/Context/exercise/utils.js b/src/components/patterns/Context/exercise/utils.js index dfac43e..d5d8372 100644 --- a/src/components/patterns/Context/exercise/utils.js +++ b/src/components/patterns/Context/exercise/utils.js @@ -1,6 +1,8 @@ export function hashGql(query, variables) { const body = JSON.stringify({ query, variables }); + console.log("hashGql: no hit from the cache"); + return body.split("").reduce(function (a, b) { a = (a << 5) - a + b.charCodeAt(0); return a & a; diff --git a/src/components/patterns/CustomHooks/exercise/useWidth.js b/src/components/patterns/CustomHooks/exercise/useWidth.js index 83c8272..86748e1 100644 --- a/src/components/patterns/CustomHooks/exercise/useWidth.js +++ b/src/components/patterns/CustomHooks/exercise/useWidth.js @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback } from "react"; +import React, { useState, useEffect } from "react"; export const SMALL = 1; export const MEDIUM = 2; @@ -26,26 +26,28 @@ const windowWidth = () => { const useWidth = () => { const [width, setWidth] = useState(null); - const handleResize = () => { - let currentWidth = windowWidth(); - if (currentWidth !== width) { + useEffect(() => { + const handleResize = () => { + let currentWidth = windowWidth(); setWidth(currentWidth); - } - }; - - const handleResizeCallback = useCallback(handleResize, []); + }; - useEffect(() => { if (window) { - window.addEventListener("resize", handleResizeCallback); - handleResizeCallback(); + window.addEventListener("resize", handleResize); + handleResize(); } return () => { - if (window) window.removeEventListener("resize", handleResizeCallback); + if (window) window.removeEventListener("resize", handleResize); }; - }, [handleResizeCallback]); + }, []); return width; }; +export const withWidth = (Component) => (props) => { + const width = useWidth(); + + return ; +}; + export default useWidth; diff --git a/src/components/patterns/HookReducer/exercise/index.jsx b/src/components/patterns/HookReducer/exercise/index.jsx index dad97cf..d745f97 100644 --- a/src/components/patterns/HookReducer/exercise/index.jsx +++ b/src/components/patterns/HookReducer/exercise/index.jsx @@ -1,10 +1,12 @@ -/* eslint-disable no-unused-vars */ import React from "react"; function reducer(state, action) { switch (action.type) { - // 🚧 Add a SET_ERRORS case that adds an errors key to the state with the action.payload - // 🕵️‍♀️ You probably want to clear previous errors every time you do SET_ERRORS + case "SET_ERRORS": + return { + ...state, + errors: action.payload, + }; case "SET_FIELD_VALUE": return { ...state, @@ -18,31 +20,18 @@ function reducer(state, action) { } } -function LoginForm(props) { - const { initialValues, onSubmit } = props; - // 👮‍♀you don't have to edit this validate function - const validate = (values) => { - let errors = {}; - if (!values.password) { - errors.password = "Password is required"; - } - if (!values.userId) { - errors.userId = "User Id is required"; - } - return errors; - }; - +function useForm(props) { const [state, dispatch] = React.useReducer(reducer, { - values: initialValues, + values: props.initialValues, errors: {}, }); React.useEffect(() => { - if (validate) { - const errors = validate(state.values); - // 🚧 dispatch a SET_ERRORS action with the errors as payload + if (props.validate) { + const errors = props.validate(state.values); + dispatch({ type: "SET_ERRORS", payload: errors }); } - }, []); // 🚧 dispatch the SET_ERRORS action only when the state of the input fields change. + }, [state.values]); const handleChange = (fieldName) => (event) => { event.preventDefault(); @@ -54,9 +43,9 @@ function LoginForm(props) { const handleSubmit = (event) => { event.preventDefault(); - const errors = validate(state.values); + const errors = props.validate(state.values); if (!Object.keys(errors).length) { - onSubmit(state.values); + props.onSubmit(state.values); } }; @@ -65,15 +54,36 @@ function LoginForm(props) { onChange: handleChange(fieldName), }); - const { errors } = state; + return { handleChange, handleSubmit, getFieldProps, errors: state.errors }; +} + +function LoginForm(props) { + const form = useForm({ + initialValues: props.initialValues, + onSubmit: async (values) => { + alert(JSON.stringify(values, null, 2)); + }, + validate: (values) => { + let errors = {}; + if (!values.password) { + errors.password = "Password is required"; + } + if (!values.email) { + errors.email = "Email is required"; + } + return errors; + }, + }); + + const { handleSubmit, getFieldProps, errors = {} } = form; return (


From f61faf8f062822f9ffc8da8bf155781a0d355c44 Mon Sep 17 00:00:00 2001 From: alexlbr Date: Wed, 14 Oct 2020 19:59:40 +0200 Subject: [PATCH 8/9] update solution hook reducer --- .../memoization/exercise.js | 2 +- src/components/patterns/HookReducer/Page.jsx | 50 ++++++++- .../patterns/HookReducer/exercise/index.jsx | 101 ++++++++++-------- 3 files changed, 108 insertions(+), 45 deletions(-) diff --git a/src/components/functional-programming/memoization/exercise.js b/src/components/functional-programming/memoization/exercise.js index 73b0352..cd6da45 100644 --- a/src/components/functional-programming/memoization/exercise.js +++ b/src/components/functional-programming/memoization/exercise.js @@ -33,7 +33,7 @@ export function doAnyWork(amount = 1, amount2 = 1, amount3 = 1) { const resolverFn = (args) => args.join(","); -function memoize(fn, resolver = resolverFn) { +export function memoize(fn, resolver = resolverFn) { let cache = {}; return (...args) => { const key = resolver(args); diff --git a/src/components/patterns/HookReducer/Page.jsx b/src/components/patterns/HookReducer/Page.jsx index d87d090..78d0ca6 100644 --- a/src/components/patterns/HookReducer/Page.jsx +++ b/src/components/patterns/HookReducer/Page.jsx @@ -46,7 +46,7 @@ const Page = () => ( know.

-

Exercise

+

Exercise part 1

🎯 The goal is to understand how to handle complex state logic in our @@ -75,7 +75,7 @@ const Page = () => (
-

Bonus exercise

+

Exercise part 2

Create a custom hook from your Login Form. You can call it useForm. @@ -87,6 +87,52 @@ const Page = () => ( Don't think of state only, but also functions that create "props". + +

Exercise part 3

+

+ By default we are displaying the error message to the user even if the + user did not use the form. That's not a great user experience. To improve + that we are going to add a state in our form to identify which fields have + been touched. +

+

+ A field is touched if this field has ever gained and lost focus. false + otherwise. +

+ +

Bonus exercise part 1

+

+ We are going to add some state to our form to know when the form is being + submitted. +

+

+ If the form is being submitted then we'll display the text "submitting" + instead of "submit" in the submit button. +

+ +

Bonus exercise part 2

+

+ Use the{" "} + + React Profiler + {" "} + in the React Dev Tools to record what happens when you type in the user + id. Is the password being rendered as well? Why? +

+

+ To avoid unnecessary renders you can create another component called + "Field" and use{" "} + + React.memo + + . +

); diff --git a/src/components/patterns/HookReducer/exercise/index.jsx b/src/components/patterns/HookReducer/exercise/index.jsx index 12909ea..b809dae 100644 --- a/src/components/patterns/HookReducer/exercise/index.jsx +++ b/src/components/patterns/HookReducer/exercise/index.jsx @@ -14,38 +14,36 @@ function reducer(state, action) { ...state.values, ...action.payload, }, - dirtyFields: { - ...state.dirtyFields, - ...getDirtyFields(action.payload, state.initialValues), + touched: { + ...state.touched, + ...getTouchedFields(action.payload, true), }, }; + case "SUBMITTING_FORM": + return { + ...state, + submitting: action.payload, + }; default: return state; } } -function getDirtyFields(values = {}, initialValues = {}) { +function getTouchedFields(values = {}, touched = false) { return Object.keys(values).reduce((acc, key) => { - acc[key] = values[key] !== (initialValues[key] || ""); + acc[key] = touched; return acc; }, {}); } -function getInitialState(initialValues = {}) { - return { - values: initialValues, - initialValues, - dirtyFields: getDirtyFields(initialValues), - errors: {}, - }; -} - function useForm(props) { - const [state, dispatch] = React.useReducer( - reducer, - getInitialState(props.values) - ); + const [state, dispatch] = React.useReducer(reducer, { + values: props.initialValues, + touched: getTouchedFields(props.initialValues), + errors: {}, + submitting: false, + }); React.useEffect(() => { if (props.validate) { @@ -62,17 +60,22 @@ function useForm(props) { }); }; - const handleSubmit = (event) => { + const handleSubmit = async (event) => { event.preventDefault(); const errors = props.validate(state.values); if (!Object.keys(errors).length) { - props.onSubmit(state.values); + dispatch({ type: "SUBMITTING_FORM", payload: true }); + // adding await in case onSubmit returns a promise, which is most likely in a real-world scenario + await props.onSubmit(state.values); + dispatch({ type: "SUBMITTING_FORM", payload: false }); } }; + // useMemo wouldn't work here to keep the identity of the returned props because we need to memoize based on state.values[fieldName] and the fieldName is not known ahead of time const getFieldProps = (fieldName) => ({ value: state.values[fieldName], onChange: handleChange(fieldName), + meta: { error: state.errors[fieldName], touched: state.touched[fieldName] }, }); return { @@ -80,14 +83,32 @@ function useForm(props) { handleSubmit, getFieldProps, errors: state.errors, - dirtyFields: state.dirtyFields, + touched: state.touched, + submitting: state.submitting, }; } +// React.memo doesn't work here because the function getFieldProps generates a new reference for the props object +const Field = React.memo(({ component: Component, ...rest }) => ( + +)); + +const Input = ({ meta, label, ...rest }) => ( + +); + function LoginForm(props) { const form = useForm({ initialValues: props.initialValues, onSubmit: async (values) => { + await new Promise((resolve) => setTimeout(resolve, 1000)); alert(JSON.stringify(values, null, 2)); }, validate: (values) => { @@ -95,36 +116,32 @@ function LoginForm(props) { if (!values.password) { errors.password = "Password is required"; } - if (!values.email) { - errors.email = "Email is required"; + if (!values.userId) { + errors.userId = "UserId is required"; } return errors; }, }); - const { handleSubmit, getFieldProps, errors = {}, dirtyFields } = form; + const { handleSubmit, getFieldProps, submitting } = form; return ( - +
- +
- + ); } @@ -135,7 +152,7 @@ const Exercise = () => ( From 3ab3eca1a6e318b165911b088326e041ed0b8242 Mon Sep 17 00:00:00 2001 From: alexlbr Date: Wed, 14 Oct 2020 20:17:35 +0200 Subject: [PATCH 9/9] update context and hook reducer answers --- src/components/patterns/Context/Page.jsx | 40 ++++++++++++------ .../patterns/Context/exercise/index.jsx | 42 ++++++++++++++++--- src/components/patterns/HookReducer/Page.jsx | 4 ++ 3 files changed, 68 insertions(+), 18 deletions(-) diff --git a/src/components/patterns/Context/Page.jsx b/src/components/patterns/Context/Page.jsx index bd0fcd7..07882af 100644 --- a/src/components/patterns/Context/Page.jsx +++ b/src/components/patterns/Context/Page.jsx @@ -22,21 +22,37 @@ const Page = () => (
-

Bonus Exercise

+ +

Bonus Exercise 1

+

Now that you know how the React Context works:

+
    +
  • + Would use the React Context for the form in + the previous React Reducer Exercise? What are the pros and cons? +
  • +
  • + If you use the React Context for the form, + would you pass the value of the field and other props to the Field + component using context or props? +
  • +
  • + If you pass the value of the "input" and the + other required props to the Field component using the React Context, do + you think it still makes sense to use the React.memo HoC in the Field + component? +
  • +
+ +

Bonus Exercise 2

- - b.1. In{" "} - - src/components/patterns/Context/exercise/GraphQLProvider.jsx - {" "} - we are using const memoizedHashGql = memoize(hashGql);. - Should we use useMemo instead? Why? + In our current implementation the cache (data + key in our reducer) for each pair query & variables, we can only send 1 + query at a time. How would you make it possible to send requests + concurrently?

- - b.2. In our current implementation, although there is a cache (data key in - our reducer) for each pair query & variables, we can only send 1 query at - a time. How would you make it possible to send requests concurrently? + Answer: You need to move the loading state and the error state inside the + hash key that contains the data.

); diff --git a/src/components/patterns/Context/exercise/index.jsx b/src/components/patterns/Context/exercise/index.jsx index 764b1a5..9958ea8 100644 --- a/src/components/patterns/Context/exercise/index.jsx +++ b/src/components/patterns/Context/exercise/index.jsx @@ -2,7 +2,6 @@ import React from "react"; import { useQuery } from "./GraphQLProvider"; -const variables = { id: 2 }; const Root = () => { const { data, loading, error } = useQuery( `query character($id: ID! = 1) { @@ -11,7 +10,7 @@ const Root = () => { name } }`, - { variables } + { variables: { id: 2 } } ); if (loading) { return "loading"; @@ -65,10 +64,9 @@ const Root = () => { your useQuery. You can create a handy useClient custom hook like we did in the example{" "} - src/components/patterns/Context/example/modal.jsx -> useModal function{" "} + src/components/patterns/Context/example/modal.jsx : useModal function{" "}

-

Tasks part 2:

In large component trees, an alternative we recommend is to pass down a @@ -89,9 +87,41 @@ const Root = () => {

🤔 React docs say "use two different context types". Let's do it!

- Task: create two different context types for our StoreContext. One - context for the dispatch, and another context for the state. + 2.1. Create two different context types for + our StoreContext. One context for the dispatch, and another context for + the state. +

+

+ 2.2. Great! We've implemented task 2.1. but, + wait 🤔... does it make any difference in our use case? Why? Discuss + with your peers. +

+

+ It doesn't make sense in our case because we are using both dispatch and + the state from the reducer in the same hook. It would make sense if we + had two different hooks used in separately in different components in + the app. +

+

Tasks part 3:

+

+ 3. In{" "} + + src/components/patterns/Context/exercise/GraphQLProvider.jsx + {" "} + we are using const memoizedHashGql = memoize(hashGql);. + Should we use useMemo instead? Why?

+

Answer: React.useMemo would't work for two reasons:

+
    +
  1. + When the component unmounts it's not guaranteed the cache will be + preserved. +
  2. +
  3. + React.useMemo will only cache the last value, it doesn't keep a record + of all the queries and variables we might run. +
  4. +
); }; diff --git a/src/components/patterns/HookReducer/Page.jsx b/src/components/patterns/HookReducer/Page.jsx index 78d0ca6..9cd8104 100644 --- a/src/components/patterns/HookReducer/Page.jsx +++ b/src/components/patterns/HookReducer/Page.jsx @@ -133,6 +133,10 @@ const Page = () => ( .

+

+ Answer: React.memo doesn't work here because the function getFieldProps + generates a new reference for the props object in every render. +

);