diff --git a/src/components/App.jsx b/src/components/App.jsx
index 8cb093f..613c022 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 { 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";
@@ -20,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() {
@@ -143,4 +142,4 @@ class App extends React.Component {
}
}
-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..cd6da45 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(",");
+
+export 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/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 }) => (
-
+ 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/GraphQLProvider.jsx b/src/components/patterns/Context/exercise/GraphQLProvider.jsx
index 3a32bcf..89c6176 100644
--- a/src/components/patterns/Context/exercise/GraphQLProvider.jsx
+++ b/src/components/patterns/Context/exercise/GraphQLProvider.jsx
@@ -1,11 +1,12 @@
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
+const CacheContext = React.createContext();
+const DispatchGQLContext = React.createContext();
+const ClientContext = React.createContext();
const reducer = (state, action) => {
switch (action.type) {
@@ -25,6 +26,7 @@ const reducer = (state, action) => {
export const GraphQLProvider = ({
children,
+ client,
initialState = {
data: {},
error: null,
@@ -33,28 +35,30 @@ export const GraphQLProvider = ({
}) => {
const [state, dispatch] = useReducer(reducer, initialState);
- // 🚧 Part 1.2. Add your ClientProvider inside the return
return (
-
- {children}
-
+
+ {/* Anwser to task 2: It doesn't make sense in this case to split it in the following two contexts */}
+
+
+ {children}
+
+
+
);
};
-// 🚧 Bonus exercise, 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];
useEffect(() => {
- if (data) {
+ if (data || error) {
return;
}
@@ -77,8 +81,26 @@ export const useQuery = (query, { variables }) => {
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"
diff --git a/src/components/patterns/Context/exercise/index.jsx b/src/components/patterns/Context/exercise/index.jsx
index 622c1ad..9958ea8 100644
--- a/src/components/patterns/Context/exercise/index.jsx
+++ b/src/components/patterns/Context/exercise/index.jsx
@@ -64,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
@@ -88,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.
+
+
);
};
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/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} */}
-
🎯 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,56 @@ 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
+
+ .
+
+
+ Answer: React.memo doesn't work here because the function getFieldProps
+ generates a new reference for the props object in every render.
+
);
diff --git a/src/components/patterns/HookReducer/exercise/index.jsx b/src/components/patterns/HookReducer/exercise/index.jsx
index dad97cf..b809dae 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,
@@ -12,37 +14,43 @@ function reducer(state, action) {
...state.values,
...action.payload,
},
+ touched: {
+ ...state.touched,
+ ...getTouchedFields(action.payload, true),
+ },
+ };
+ case "SUBMITTING_FORM":
+ return {
+ ...state,
+ submitting: action.payload,
};
default:
return state;
}
}
-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 getTouchedFields(values = {}, touched = false) {
+ return Object.keys(values).reduce((acc, key) => {
+ acc[key] = touched;
+ return acc;
+ }, {});
+}
+
+function useForm(props) {
const [state, dispatch] = React.useReducer(reducer, {
- values: initialValues,
+ values: props.initialValues,
+ touched: getTouchedFields(props.initialValues),
errors: {},
+ submitting: false,
});
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();
@@ -52,40 +60,88 @@ function LoginForm(props) {
});
};
- const handleSubmit = (event) => {
+ const handleSubmit = async (event) => {
event.preventDefault();
- const errors = validate(state.values);
+ const errors = props.validate(state.values);
if (!Object.keys(errors).length) {
- 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] },
});
- const { errors } = state;
+ return {
+ handleChange,
+ handleSubmit,
+ getFieldProps,
+ errors: state.errors,
+ 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) => {
+ let errors = {};
+ if (!values.password) {
+ errors.password = "Password is required";
+ }
+ if (!values.userId) {
+ errors.userId = "UserId is required";
+ }
+ return errors;
+ },
+ });
+
+ const { handleSubmit, getFieldProps, submitting } = form;
return (
);
}
@@ -98,9 +154,6 @@ const Exercise = () => (
password: "",
userId: "",
}}
- onSubmit={(values) => {
- alert(JSON.stringify(values, null, 2));
- }}
/>
);
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) => (
-