+ );
+};
-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 = () => (
+
+ )}
+ />
+);
+
+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 = () => (
-
- )}
- />
-);
+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 2b4e1d8673633e00a7b462b5482eafe143d976fa Mon Sep 17 00:00:00 2001
From: alexlbr
Date: Fri, 17 Apr 2020 17:27:10 +0100
Subject: [PATCH 5/9] remove unnecessary exports and context
---
.../patterns/Context/exercise/GraphQLProvider.jsx | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/src/components/patterns/Context/exercise/GraphQLProvider.jsx b/src/components/patterns/Context/exercise/GraphQLProvider.jsx
index b31b02c..89c6176 100644
--- a/src/components/patterns/Context/exercise/GraphQLProvider.jsx
+++ b/src/components/patterns/Context/exercise/GraphQLProvider.jsx
@@ -4,10 +4,9 @@ import { memoize, hashGql } from "./utils";
const RECEIVE_DATA = "RECEIVE_DATA";
const SET_ERROR = "SET_ERROR";
-export const StoreContext = React.createContext();
-export const CacheContext = React.createContext();
-export const DispatchGQLContext = React.createContext();
-export const ClientContext = React.createContext();
+const CacheContext = React.createContext();
+const DispatchGQLContext = React.createContext();
+const ClientContext = React.createContext();
const reducer = (state, action) => {
switch (action.type) {
From af7892092ebf771c712e02b5a48e778fabf1ee35 Mon Sep 17 00:00:00 2001
From: alexlbr
Date: Tue, 13 Oct 2020 15:47:16 +0200
Subject: [PATCH 6/9] wip: useForm part 3
---
.../patterns/HookReducer/exercise/index.jsx | 29 ++++++++++++++++---
1 file changed, 25 insertions(+), 4 deletions(-)
diff --git a/src/components/patterns/HookReducer/exercise/index.jsx b/src/components/patterns/HookReducer/exercise/index.jsx
index d745f97..d471237 100644
--- a/src/components/patterns/HookReducer/exercise/index.jsx
+++ b/src/components/patterns/HookReducer/exercise/index.jsx
@@ -14,17 +14,38 @@ function reducer(state, action) {
...state.values,
...action.payload,
},
+ dirtyFields: {
+ ...state.dirtyFields,
+ ...getDirtyFields(action.payload, state.initialValues),
+ },
};
default:
return state;
}
}
-function useForm(props) {
- const [state, dispatch] = React.useReducer(reducer, {
- values: props.initialValues,
+function getDirtyFields(values = {}, initialValues = {}) {
+ return Object.keys(values).reduce((acc, key) => {
+ acc[key] = values[key] != initialValues[key];
+
+ 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)
+ );
React.useEffect(() => {
if (props.validate) {
From ae35682fd7701fee8bfd13062f0e8acbf56aabff Mon Sep 17 00:00:00 2001
From: alexlbr
Date: Tue, 13 Oct 2020 15:55:21 +0200
Subject: [PATCH 7/9] wip: useForm part 3
---
.../patterns/HookReducer/exercise/index.jsx | 18 +++++++++++++-----
1 file changed, 13 insertions(+), 5 deletions(-)
diff --git a/src/components/patterns/HookReducer/exercise/index.jsx b/src/components/patterns/HookReducer/exercise/index.jsx
index d471237..12909ea 100644
--- a/src/components/patterns/HookReducer/exercise/index.jsx
+++ b/src/components/patterns/HookReducer/exercise/index.jsx
@@ -26,7 +26,7 @@ function reducer(state, action) {
function getDirtyFields(values = {}, initialValues = {}) {
return Object.keys(values).reduce((acc, key) => {
- acc[key] = values[key] != initialValues[key];
+ acc[key] = values[key] !== (initialValues[key] || "");
return acc;
}, {});
@@ -75,7 +75,13 @@ function useForm(props) {
onChange: handleChange(fieldName),
});
- return { handleChange, handleSubmit, getFieldProps, errors: state.errors };
+ return {
+ handleChange,
+ handleSubmit,
+ getFieldProps,
+ errors: state.errors,
+ dirtyFields: state.dirtyFields,
+ };
}
function LoginForm(props) {
@@ -96,7 +102,7 @@ function LoginForm(props) {
},
});
- const { handleSubmit, getFieldProps, errors = {} } = form;
+ const { handleSubmit, getFieldProps, errors = {}, dirtyFields } = form;
return (
-
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:
+
+
+ When the component unmounts it's not guaranteed the cache will be
+ preserved.
+
+
+ React.useMemo will only cache the last value, it doesn't keep a record
+ of all the queries and variables we might run.
+