├── .gitattributes ├── epicshop ├── .diffignore ├── .npmrc ├── tsconfig.json ├── post-set-playground.js ├── in-browser-tests.spec.js ├── update-deps.sh ├── package.json ├── Dockerfile ├── setup-custom.js ├── playwright.config.js ├── fly.yaml ├── fix-watch.js ├── setup.js └── fix.js ├── reset.d.ts ├── .npmrc ├── exercises ├── 04.slots │ ├── 03.problem.prop │ │ ├── index.css │ │ ├── index.tsx │ │ ├── text-field.tsx │ │ ├── app.tsx │ │ ├── README.mdx │ │ ├── slots.tsx │ │ └── toggle.tsx │ ├── 01.problem.context │ │ ├── index.css │ │ ├── index.tsx │ │ ├── text-field.tsx │ │ ├── app.tsx │ │ ├── slots.tsx │ │ ├── toggle.tsx │ │ └── README.mdx │ ├── 01.solution.context │ │ ├── index.css │ │ ├── index.tsx │ │ ├── README.mdx │ │ ├── text-field.tsx │ │ ├── app.tsx │ │ ├── slots.tsx │ │ ├── toggle.tsx │ │ └── index.test.tsx │ ├── 02.problem.generic │ │ ├── index.css │ │ ├── index.tsx │ │ ├── text-field.tsx │ │ ├── slots.tsx │ │ ├── app.tsx │ │ ├── README.mdx │ │ └── toggle.tsx │ ├── 02.solution.generic │ │ ├── index.css │ │ ├── index.tsx │ │ ├── README.mdx │ │ ├── text-field.tsx │ │ ├── app.tsx │ │ ├── slots.tsx │ │ ├── index.test.tsx │ │ └── toggle.tsx │ ├── 03.solution.prop │ │ ├── index.css │ │ ├── index.tsx │ │ ├── text-field.tsx │ │ ├── README.mdx │ │ ├── app.tsx │ │ ├── toggle.tsx │ │ ├── slots.tsx │ │ └── index.test.tsx │ ├── FINISHED.mdx │ └── README.mdx ├── 05.prop-getters │ ├── 02.problem.getters │ │ ├── index.css │ │ ├── index.tsx │ │ ├── app.tsx │ │ ├── toggle.tsx │ │ └── README.mdx │ ├── 01.problem.collections │ │ ├── index.css │ │ ├── index.tsx │ │ ├── toggle.tsx │ │ ├── app.tsx │ │ └── README.mdx │ ├── 01.solution.collections │ │ ├── index.css │ │ ├── toggle.test.tsx │ │ ├── custom-button.test.tsx │ │ ├── index.tsx │ │ ├── toggle.tsx │ │ ├── README.mdx │ │ └── app.tsx │ ├── 02.solution.getters │ │ ├── index.css │ │ ├── toggle.test.tsx │ │ ├── custom-button.test.tsx │ │ ├── index.tsx │ │ ├── README.mdx │ │ ├── app.tsx │ │ ├── toggle.tsx │ │ └── forwards-props.test.tsx │ ├── FINISHED.mdx │ └── README.mdx ├── 07.state-reducer │ ├── 01.problem.reducer │ │ ├── index.css │ │ ├── index.tsx │ │ ├── README.mdx │ │ ├── app.tsx │ │ └── toggle.tsx │ ├── 01.solution.reducer │ │ ├── index.css │ │ ├── toggle.test.tsx │ │ ├── index.tsx │ │ ├── README.mdx │ │ ├── app.tsx │ │ ├── toggle.tsx │ │ └── click-limit.test.tsx │ ├── 02.problem.default │ │ ├── index.css │ │ ├── index.tsx │ │ ├── README.mdx │ │ ├── app.tsx │ │ └── toggle.tsx │ ├── 02.solution.default │ │ ├── index.css │ │ ├── toggle.test.tsx │ │ ├── index.tsx │ │ ├── exporting-toggle-reducer.test.tsx │ │ ├── README.mdx │ │ ├── app.tsx │ │ ├── toggle.tsx │ │ └── click-limit.test.tsx │ ├── FINISHED.mdx │ └── README.mdx ├── 08.control-props │ ├── 01.problem.control │ │ ├── index.css │ │ ├── index.tsx │ │ ├── README.mdx │ │ └── app.tsx │ ├── 01.solution.control │ │ ├── index.css │ │ ├── index.tsx │ │ ├── README.mdx │ │ ├── uncontrolled.test.tsx │ │ ├── synchronized.test.tsx │ │ ├── app.tsx │ │ └── toggle.tsx │ ├── FINISHED.mdx │ └── README.mdx ├── 03.compound-components │ ├── 01.problem.context │ │ ├── index.css │ │ ├── index.tsx │ │ ├── app.tsx │ │ ├── toggle.tsx │ │ └── README.mdx │ ├── 01.solution.context │ │ ├── index.css │ │ ├── toggle.test.tsx │ │ ├── index.tsx │ │ ├── app.tsx │ │ ├── README.mdx │ │ └── toggle.tsx │ ├── 02.problem.validation │ │ ├── index.css │ │ ├── index.tsx │ │ ├── app.tsx │ │ ├── README.mdx │ │ └── toggle.tsx │ ├── 02.solution.validation │ │ ├── index.css │ │ ├── toggle.test.tsx │ │ ├── index.tsx │ │ ├── app.tsx │ │ ├── README.mdx │ │ ├── toggle.tsx │ │ └── validation.test.tsx │ ├── FINISHED.mdx │ └── README.mdx ├── 06.state-initializers │ ├── 01.problem.initial │ │ ├── index.css │ │ ├── index.tsx │ │ ├── app.tsx │ │ ├── README.mdx │ │ └── toggle.tsx │ ├── 01.solution.initial │ │ ├── index.css │ │ ├── index.tsx │ │ ├── README.mdx │ │ ├── app.tsx │ │ ├── index.test.tsx │ │ └── toggle.tsx │ ├── 02.problem.stability │ │ ├── index.css │ │ ├── index.tsx │ │ ├── app.tsx │ │ ├── README.mdx │ │ └── toggle.tsx │ ├── 02.solution.stability │ │ ├── index.css │ │ ├── README.mdx │ │ ├── index.tsx │ │ ├── app.tsx │ │ ├── toggle.tsx │ │ └── index.test.tsx │ ├── FINISHED.mdx │ └── README.mdx ├── 02.latest-ref │ ├── FINISHED.mdx │ ├── 01.solution.ref │ │ ├── README.mdx │ │ ├── increments.test.tsx │ │ ├── step-change.test.tsx │ │ └── index.tsx │ └── 01.problem.ref │ │ ├── index.tsx │ │ └── README.mdx ├── FINISHED.mdx ├── 01.composition │ ├── FINISHED.mdx │ ├── 01.solution.compose │ │ ├── README.mdx │ │ ├── ui-still-works.test.tsx │ │ ├── index.css │ │ └── index.tsx │ ├── 01.problem.compose │ │ ├── README.mdx │ │ ├── index.css │ │ └── index.tsx │ └── README.mdx └── README.mdx ├── public ├── favicon.ico ├── img │ ├── ski.png │ ├── kody.png │ ├── soccer.png │ ├── onewheel.png │ └── snowboard.png ├── og │ └── background.png ├── images │ └── instructor.png ├── manifest.json ├── favicon.svg ├── switch.styles.css └── logo.svg ├── .vscode ├── extensions.json └── settings.kcd.json ├── tsconfig.json ├── LICENSE.md ├── shared ├── types.d.ts ├── switch.tsx ├── utils.tsx ├── sports.tsx └── toggle.test.tsx ├── .gitignore ├── eslint.config.js ├── .github └── workflows │ └── validate.yml ├── package.json └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /epicshop/.diffignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | *.test.* -------------------------------------------------------------------------------- /reset.d.ts: -------------------------------------------------------------------------------- 1 | import '@epic-web/config/reset.d.ts' -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | legacy-peer-deps=true 2 | registry=https://registry.npmjs.org/ 3 | -------------------------------------------------------------------------------- /exercises/04.slots/03.problem.prop/index.css: -------------------------------------------------------------------------------- 1 | @import '/switch.styles.css'; 2 | -------------------------------------------------------------------------------- /epicshop/.npmrc: -------------------------------------------------------------------------------- 1 | legacy-peer-deps=true 2 | registry=https://registry.npmjs.org/ 3 | -------------------------------------------------------------------------------- /exercises/04.slots/01.problem.context/index.css: -------------------------------------------------------------------------------- 1 | @import '/switch.styles.css'; 2 | -------------------------------------------------------------------------------- /exercises/04.slots/01.solution.context/index.css: -------------------------------------------------------------------------------- 1 | @import '/switch.styles.css'; 2 | -------------------------------------------------------------------------------- /exercises/04.slots/02.problem.generic/index.css: -------------------------------------------------------------------------------- 1 | @import '/switch.styles.css'; 2 | -------------------------------------------------------------------------------- /exercises/04.slots/02.solution.generic/index.css: -------------------------------------------------------------------------------- 1 | @import '/switch.styles.css'; 2 | -------------------------------------------------------------------------------- /exercises/04.slots/03.solution.prop/index.css: -------------------------------------------------------------------------------- 1 | @import '/switch.styles.css'; 2 | -------------------------------------------------------------------------------- /exercises/05.prop-getters/02.problem.getters/index.css: -------------------------------------------------------------------------------- 1 | @import '/switch.styles.css'; 2 | -------------------------------------------------------------------------------- /exercises/05.prop-getters/01.problem.collections/index.css: -------------------------------------------------------------------------------- 1 | @import '/switch.styles.css'; 2 | -------------------------------------------------------------------------------- /exercises/05.prop-getters/01.solution.collections/index.css: -------------------------------------------------------------------------------- 1 | @import '/switch.styles.css'; 2 | -------------------------------------------------------------------------------- /exercises/05.prop-getters/02.solution.getters/index.css: -------------------------------------------------------------------------------- 1 | @import '/switch.styles.css'; 2 | -------------------------------------------------------------------------------- /exercises/07.state-reducer/01.problem.reducer/index.css: -------------------------------------------------------------------------------- 1 | @import '/switch.styles.css'; 2 | -------------------------------------------------------------------------------- /exercises/07.state-reducer/01.solution.reducer/index.css: -------------------------------------------------------------------------------- 1 | @import '/switch.styles.css'; 2 | -------------------------------------------------------------------------------- /exercises/07.state-reducer/02.problem.default/index.css: -------------------------------------------------------------------------------- 1 | @import '/switch.styles.css'; 2 | -------------------------------------------------------------------------------- /exercises/07.state-reducer/02.solution.default/index.css: -------------------------------------------------------------------------------- 1 | @import '/switch.styles.css'; 2 | -------------------------------------------------------------------------------- /exercises/08.control-props/01.problem.control/index.css: -------------------------------------------------------------------------------- 1 | @import '/switch.styles.css'; 2 | -------------------------------------------------------------------------------- /exercises/08.control-props/01.solution.control/index.css: -------------------------------------------------------------------------------- 1 | @import '/switch.styles.css'; 2 | -------------------------------------------------------------------------------- /exercises/03.compound-components/01.problem.context/index.css: -------------------------------------------------------------------------------- 1 | @import '/switch.styles.css'; 2 | -------------------------------------------------------------------------------- /exercises/03.compound-components/01.solution.context/index.css: -------------------------------------------------------------------------------- 1 | @import '/switch.styles.css'; 2 | -------------------------------------------------------------------------------- /exercises/03.compound-components/02.problem.validation/index.css: -------------------------------------------------------------------------------- 1 | @import '/switch.styles.css'; 2 | -------------------------------------------------------------------------------- /exercises/06.state-initializers/01.problem.initial/index.css: -------------------------------------------------------------------------------- 1 | @import '/switch.styles.css'; 2 | -------------------------------------------------------------------------------- /exercises/06.state-initializers/01.solution.initial/index.css: -------------------------------------------------------------------------------- 1 | @import '/switch.styles.css'; 2 | -------------------------------------------------------------------------------- /exercises/06.state-initializers/02.problem.stability/index.css: -------------------------------------------------------------------------------- 1 | @import '/switch.styles.css'; 2 | -------------------------------------------------------------------------------- /exercises/06.state-initializers/02.solution.stability/index.css: -------------------------------------------------------------------------------- 1 | @import '/switch.styles.css'; 2 | -------------------------------------------------------------------------------- /exercises/03.compound-components/02.solution.validation/index.css: -------------------------------------------------------------------------------- 1 | @import '/switch.styles.css'; 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/advanced-react-patterns/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/img/ski.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/advanced-react-patterns/HEAD/public/img/ski.png -------------------------------------------------------------------------------- /public/img/kody.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/advanced-react-patterns/HEAD/public/img/kody.png -------------------------------------------------------------------------------- /public/img/soccer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/advanced-react-patterns/HEAD/public/img/soccer.png -------------------------------------------------------------------------------- /public/img/onewheel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/advanced-react-patterns/HEAD/public/img/onewheel.png -------------------------------------------------------------------------------- /public/img/snowboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/advanced-react-patterns/HEAD/public/img/snowboard.png -------------------------------------------------------------------------------- /public/og/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/advanced-react-patterns/HEAD/public/og/background.png -------------------------------------------------------------------------------- /public/images/instructor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/advanced-react-patterns/HEAD/public/images/instructor.png -------------------------------------------------------------------------------- /epicshop/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["**/*.ts", "**/*.tsx", "**/*.js"], 3 | "extends": ["@epic-web/config/typescript"] 4 | } 5 | -------------------------------------------------------------------------------- /exercises/05.prop-getters/02.solution.getters/toggle.test.tsx: -------------------------------------------------------------------------------- 1 | import { verifySimpleToggle } from '#shared/toggle.test.tsx' 2 | import '.' 3 | 4 | await verifySimpleToggle() 5 | -------------------------------------------------------------------------------- /exercises/07.state-reducer/01.solution.reducer/toggle.test.tsx: -------------------------------------------------------------------------------- 1 | import { verifySimpleToggle } from '#shared/toggle.test.tsx' 2 | import '.' 3 | 4 | await verifySimpleToggle() 5 | -------------------------------------------------------------------------------- /exercises/07.state-reducer/02.solution.default/toggle.test.tsx: -------------------------------------------------------------------------------- 1 | import { verifySimpleToggle } from '#shared/toggle.test.tsx' 2 | import '.' 3 | 4 | await verifySimpleToggle() 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "esbenp.prettier-vscode", 5 | "VisualStudioExptTeam.vscodeintellicode" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /exercises/05.prop-getters/01.solution.collections/toggle.test.tsx: -------------------------------------------------------------------------------- 1 | import { verifySimpleToggle } from '#shared/toggle.test.tsx' 2 | import '.' 3 | 4 | await verifySimpleToggle() 5 | -------------------------------------------------------------------------------- /exercises/03.compound-components/01.solution.context/toggle.test.tsx: -------------------------------------------------------------------------------- 1 | import { verifySimpleToggleWithText } from '#shared/toggle.test.tsx' 2 | import '.' 3 | 4 | await verifySimpleToggleWithText() 5 | -------------------------------------------------------------------------------- /exercises/03.compound-components/02.solution.validation/toggle.test.tsx: -------------------------------------------------------------------------------- 1 | import { verifySimpleToggleWithText } from '#shared/toggle.test.tsx' 2 | import '.' 3 | 4 | await verifySimpleToggleWithText() 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["**/*.ts", "**/*.tsx"], 3 | "extends": ["@epic-web/config/typescript"], 4 | "compilerOptions": { 5 | "paths": { 6 | "#shared/*": ["./shared/*"] 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /exercises/02.latest-ref/FINISHED.mdx: -------------------------------------------------------------------------------- 1 | # Latest Ref 2 | 3 | 4 | 5 | 👨‍💼 Great job! You now know the latest ref pattern. 6 | -------------------------------------------------------------------------------- /exercises/FINISHED.mdx: -------------------------------------------------------------------------------- 1 | # Advanced React Patterns 🤯 2 | 3 | 4 | 5 | 👨‍💼 Congratulations. You've made it to the end. Awesome work! 6 | -------------------------------------------------------------------------------- /epicshop/post-set-playground.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | import path from 'node:path' 3 | 4 | fs.writeFileSync( 5 | path.join(process.env.EPICSHOP_PLAYGROUND_DEST_DIR, 'tsconfig.json'), 6 | JSON.stringify({ extends: '../tsconfig' }, null, 2), 7 | ) 8 | -------------------------------------------------------------------------------- /exercises/04.slots/01.problem.context/index.tsx: -------------------------------------------------------------------------------- 1 | import * as ReactDOM from 'react-dom/client' 2 | import { App } from './app.tsx' 3 | 4 | const rootEl = document.createElement('div') 5 | document.body.append(rootEl) 6 | ReactDOM.createRoot(rootEl).render() 7 | -------------------------------------------------------------------------------- /exercises/04.slots/01.solution.context/index.tsx: -------------------------------------------------------------------------------- 1 | import * as ReactDOM from 'react-dom/client' 2 | import { App } from './app.tsx' 3 | 4 | const rootEl = document.createElement('div') 5 | document.body.append(rootEl) 6 | ReactDOM.createRoot(rootEl).render() 7 | -------------------------------------------------------------------------------- /exercises/04.slots/02.problem.generic/index.tsx: -------------------------------------------------------------------------------- 1 | import * as ReactDOM from 'react-dom/client' 2 | import { App } from './app.tsx' 3 | 4 | const rootEl = document.createElement('div') 5 | document.body.append(rootEl) 6 | ReactDOM.createRoot(rootEl).render() 7 | -------------------------------------------------------------------------------- /exercises/04.slots/02.solution.generic/index.tsx: -------------------------------------------------------------------------------- 1 | import * as ReactDOM from 'react-dom/client' 2 | import { App } from './app.tsx' 3 | 4 | const rootEl = document.createElement('div') 5 | document.body.append(rootEl) 6 | ReactDOM.createRoot(rootEl).render() 7 | -------------------------------------------------------------------------------- /exercises/04.slots/03.problem.prop/index.tsx: -------------------------------------------------------------------------------- 1 | import * as ReactDOM from 'react-dom/client' 2 | import { App } from './app.tsx' 3 | 4 | const rootEl = document.createElement('div') 5 | document.body.append(rootEl) 6 | ReactDOM.createRoot(rootEl).render() 7 | -------------------------------------------------------------------------------- /exercises/04.slots/03.solution.prop/index.tsx: -------------------------------------------------------------------------------- 1 | import * as ReactDOM from 'react-dom/client' 2 | import { App } from './app.tsx' 3 | 4 | const rootEl = document.createElement('div') 5 | document.body.append(rootEl) 6 | ReactDOM.createRoot(rootEl).render() 7 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | This material is available for private, non-commercial use under the 2 | [GPL version 3](http://www.gnu.org/licenses/gpl-3.0-standalone.html). If you 3 | would like to use this material to conduct your own workshop, please contact us 4 | at team@epicweb.dev 5 | -------------------------------------------------------------------------------- /exercises/01.composition/FINISHED.mdx: -------------------------------------------------------------------------------- 1 | # Composition 2 | 3 | 4 | 5 | 👨‍💼 Great work! You now know how to create layout components to reduce prop 6 | drilling! 7 | -------------------------------------------------------------------------------- /exercises/05.prop-getters/02.solution.getters/custom-button.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen } from '@testing-library/dom' 2 | import { verifyIsToggle } from '#shared/toggle.test.tsx' 3 | import '.' 4 | 5 | await verifyIsToggle(await screen.findByLabelText('custom-button')) 6 | -------------------------------------------------------------------------------- /exercises/05.prop-getters/01.problem.collections/index.tsx: -------------------------------------------------------------------------------- 1 | import * as ReactDOM from 'react-dom/client' 2 | import { App } from './app.tsx' 3 | 4 | const rootEl = document.createElement('div') 5 | document.body.append(rootEl) 6 | ReactDOM.createRoot(rootEl).render() 7 | -------------------------------------------------------------------------------- /exercises/05.prop-getters/01.solution.collections/custom-button.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen } from '@testing-library/dom' 2 | import { verifyIsToggle } from '#shared/toggle.test.tsx' 3 | import '.' 4 | 5 | await verifyIsToggle(await screen.findByLabelText('custom-button')) 6 | -------------------------------------------------------------------------------- /exercises/05.prop-getters/02.problem.getters/index.tsx: -------------------------------------------------------------------------------- 1 | import * as ReactDOM from 'react-dom/client' 2 | import { App } from './app.tsx' 3 | 4 | const rootEl = document.createElement('div') 5 | document.body.append(rootEl) 6 | ReactDOM.createRoot(rootEl).render() 7 | -------------------------------------------------------------------------------- /exercises/05.prop-getters/02.solution.getters/index.tsx: -------------------------------------------------------------------------------- 1 | import * as ReactDOM from 'react-dom/client' 2 | import { App } from './app.tsx' 3 | 4 | const rootEl = document.createElement('div') 5 | document.body.append(rootEl) 6 | ReactDOM.createRoot(rootEl).render() 7 | -------------------------------------------------------------------------------- /exercises/06.state-initializers/02.solution.stability/README.mdx: -------------------------------------------------------------------------------- 1 | # Stability 2 | 3 | 4 | 5 | 👨‍💼 Great. Now we've got some stability with our state initializer. Well done! 6 | -------------------------------------------------------------------------------- /exercises/07.state-reducer/01.problem.reducer/index.tsx: -------------------------------------------------------------------------------- 1 | import * as ReactDOM from 'react-dom/client' 2 | import { App } from './app.tsx' 3 | 4 | const rootEl = document.createElement('div') 5 | document.body.append(rootEl) 6 | ReactDOM.createRoot(rootEl).render() 7 | -------------------------------------------------------------------------------- /exercises/07.state-reducer/01.solution.reducer/index.tsx: -------------------------------------------------------------------------------- 1 | import * as ReactDOM from 'react-dom/client' 2 | import { App } from './app.tsx' 3 | 4 | const rootEl = document.createElement('div') 5 | document.body.append(rootEl) 6 | ReactDOM.createRoot(rootEl).render() 7 | -------------------------------------------------------------------------------- /exercises/07.state-reducer/02.problem.default/index.tsx: -------------------------------------------------------------------------------- 1 | import * as ReactDOM from 'react-dom/client' 2 | import { App } from './app.tsx' 3 | 4 | const rootEl = document.createElement('div') 5 | document.body.append(rootEl) 6 | ReactDOM.createRoot(rootEl).render() 7 | -------------------------------------------------------------------------------- /exercises/07.state-reducer/02.solution.default/index.tsx: -------------------------------------------------------------------------------- 1 | import * as ReactDOM from 'react-dom/client' 2 | import { App } from './app.tsx' 3 | 4 | const rootEl = document.createElement('div') 5 | document.body.append(rootEl) 6 | ReactDOM.createRoot(rootEl).render() 7 | -------------------------------------------------------------------------------- /exercises/08.control-props/01.problem.control/index.tsx: -------------------------------------------------------------------------------- 1 | import * as ReactDOM from 'react-dom/client' 2 | import { App } from './app.tsx' 3 | 4 | const rootEl = document.createElement('div') 5 | document.body.append(rootEl) 6 | ReactDOM.createRoot(rootEl).render() 7 | -------------------------------------------------------------------------------- /exercises/08.control-props/01.solution.control/index.tsx: -------------------------------------------------------------------------------- 1 | import * as ReactDOM from 'react-dom/client' 2 | import { App } from './app.tsx' 3 | 4 | const rootEl = document.createElement('div') 5 | document.body.append(rootEl) 6 | ReactDOM.createRoot(rootEl).render() 7 | -------------------------------------------------------------------------------- /exercises/03.compound-components/01.problem.context/index.tsx: -------------------------------------------------------------------------------- 1 | import * as ReactDOM from 'react-dom/client' 2 | import { App } from './app.tsx' 3 | 4 | const rootEl = document.createElement('div') 5 | document.body.append(rootEl) 6 | ReactDOM.createRoot(rootEl).render() 7 | -------------------------------------------------------------------------------- /exercises/03.compound-components/01.solution.context/index.tsx: -------------------------------------------------------------------------------- 1 | import * as ReactDOM from 'react-dom/client' 2 | import { App } from './app.tsx' 3 | 4 | const rootEl = document.createElement('div') 5 | document.body.append(rootEl) 6 | ReactDOM.createRoot(rootEl).render() 7 | -------------------------------------------------------------------------------- /exercises/05.prop-getters/01.solution.collections/index.tsx: -------------------------------------------------------------------------------- 1 | import * as ReactDOM from 'react-dom/client' 2 | import { App } from './app.tsx' 3 | 4 | const rootEl = document.createElement('div') 5 | document.body.append(rootEl) 6 | ReactDOM.createRoot(rootEl).render() 7 | -------------------------------------------------------------------------------- /exercises/06.state-initializers/01.problem.initial/index.tsx: -------------------------------------------------------------------------------- 1 | import * as ReactDOM from 'react-dom/client' 2 | import { App } from './app.tsx' 3 | 4 | const rootEl = document.createElement('div') 5 | document.body.append(rootEl) 6 | ReactDOM.createRoot(rootEl).render() 7 | -------------------------------------------------------------------------------- /exercises/06.state-initializers/01.solution.initial/index.tsx: -------------------------------------------------------------------------------- 1 | import * as ReactDOM from 'react-dom/client' 2 | import { App } from './app.tsx' 3 | 4 | const rootEl = document.createElement('div') 5 | document.body.append(rootEl) 6 | ReactDOM.createRoot(rootEl).render() 7 | -------------------------------------------------------------------------------- /exercises/06.state-initializers/02.problem.stability/index.tsx: -------------------------------------------------------------------------------- 1 | import * as ReactDOM from 'react-dom/client' 2 | import { App } from './app.tsx' 3 | 4 | const rootEl = document.createElement('div') 5 | document.body.append(rootEl) 6 | ReactDOM.createRoot(rootEl).render() 7 | -------------------------------------------------------------------------------- /exercises/06.state-initializers/02.solution.stability/index.tsx: -------------------------------------------------------------------------------- 1 | import * as ReactDOM from 'react-dom/client' 2 | import { App } from './app.tsx' 3 | 4 | const rootEl = document.createElement('div') 5 | document.body.append(rootEl) 6 | ReactDOM.createRoot(rootEl).render() 7 | -------------------------------------------------------------------------------- /exercises/03.compound-components/02.problem.validation/index.tsx: -------------------------------------------------------------------------------- 1 | import * as ReactDOM from 'react-dom/client' 2 | import { App } from './app.tsx' 3 | 4 | const rootEl = document.createElement('div') 5 | document.body.append(rootEl) 6 | ReactDOM.createRoot(rootEl).render() 7 | -------------------------------------------------------------------------------- /exercises/03.compound-components/02.solution.validation/index.tsx: -------------------------------------------------------------------------------- 1 | import * as ReactDOM from 'react-dom/client' 2 | import { App } from './app.tsx' 3 | 4 | const rootEl = document.createElement('div') 5 | document.body.append(rootEl) 6 | ReactDOM.createRoot(rootEl).render() 7 | -------------------------------------------------------------------------------- /exercises/04.slots/01.solution.context/README.mdx: -------------------------------------------------------------------------------- 1 | # Slot Context 2 | 3 | 4 | 5 | 👨‍💼 That's a great start! Now we have that in place, I think we can use this in 6 | our toggle component! 7 | -------------------------------------------------------------------------------- /exercises/04.slots/FINISHED.mdx: -------------------------------------------------------------------------------- 1 | # Slots 2 | 3 | 4 | 5 | 👨‍💼 This is a really nice pattern if you find yourself building a library of 6 | components that have a lot of common components. Great job! 7 | -------------------------------------------------------------------------------- /exercises/07.state-reducer/FINISHED.mdx: -------------------------------------------------------------------------------- 1 | # State Reducer 2 | 3 | 4 | 5 | 👨‍💼 Great job! You've learned one of the simplest yet most powerful inversion of 6 | control tricks in the React biz. 7 | -------------------------------------------------------------------------------- /exercises/07.state-reducer/02.solution.default/exporting-toggle-reducer.test.tsx: -------------------------------------------------------------------------------- 1 | import { expect, testStep } from '@epic-web/workshop-utils/test' 2 | import { toggleReducer } from './toggle.tsx' 3 | 4 | await testStep('toggleReducer is exported', () => { 5 | expect(toggleReducer).to.be.a('function') 6 | }) 7 | -------------------------------------------------------------------------------- /shared/types.d.ts: -------------------------------------------------------------------------------- 1 | type SportData = { 2 | id: string 3 | name: string 4 | image: string 5 | color: string 6 | tricks: Array<{ 7 | name: string 8 | type: string 9 | points: number 10 | }> 11 | } 12 | 13 | type User = { name: string; image: string } 14 | 15 | export { SportData, User } 16 | -------------------------------------------------------------------------------- /exercises/06.state-initializers/01.solution.initial/README.mdx: -------------------------------------------------------------------------------- 1 | # Initialize Toggle 2 | 3 | 4 | 5 | 👨‍💼 Great job! Now we can initilize and reset the toggle component! But we've 6 | discovered a bug. Let's look at that next. 7 | -------------------------------------------------------------------------------- /epicshop/in-browser-tests.spec.js: -------------------------------------------------------------------------------- 1 | import { dirname, resolve } from 'path' 2 | import { fileURLToPath } from 'url' 3 | import { setupInBrowserTests } from '@epic-web/workshop-utils/playwright.server' 4 | 5 | const __dirname = dirname(fileURLToPath(import.meta.url)) 6 | process.env.EPICSHOP_CONTEXT_CWD = resolve(__dirname, '..') 7 | 8 | setupInBrowserTests() 9 | -------------------------------------------------------------------------------- /epicshop/update-deps.sh: -------------------------------------------------------------------------------- 1 | npx npm-check-updates --dep prod,dev --upgrade --root 2 | cd epicshop && npx npm-check-updates --dep prod,dev --upgrade --root 3 | cd .. 4 | rm -rf node_modules package-lock.json ./epicshop/package-lock.json ./epicshop/node_modules ./exercises/**/node_modules 5 | npm install 6 | npm run setup 7 | npm run typecheck 8 | npm run lint --fix 9 | -------------------------------------------------------------------------------- /exercises/04.slots/02.solution.generic/README.mdx: -------------------------------------------------------------------------------- 1 | # Generic Slot Components 2 | 3 | 4 | 5 | 👨‍💼 Great! Now we can reuse the `Label` component in our set of `Toggle` compound 6 | components. Let's go a step further with a generic `Text` component. 7 | -------------------------------------------------------------------------------- /exercises/05.prop-getters/01.solution.collections/toggle.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | export function useToggle() { 4 | const [on, setOn] = useState(false) 5 | const toggle = () => setOn(!on) 6 | 7 | return { 8 | on, 9 | toggle, 10 | togglerProps: { 11 | 'aria-checked': on, 12 | onClick: toggle, 13 | }, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | workspace/ 4 | **/.cache/ 5 | **/build/ 6 | **/public/build 7 | **/playwright-report 8 | **/test-results 9 | data.db 10 | /playground 11 | **/tsconfig.tsbuildinfo 12 | 13 | # in a real app you'd want to not commit the .env 14 | # file as well, but since this is for a workshop 15 | # we're going to keep them around. 16 | # .env 17 | -------------------------------------------------------------------------------- /exercises/05.prop-getters/01.solution.collections/README.mdx: -------------------------------------------------------------------------------- 1 | # Prop Collections 2 | 3 | 4 | 5 | 👨‍💼 Prop collections are great for handling common use cases for your hooks, but 6 | there's a subtle but important limitation with them that we should address next. 7 | -------------------------------------------------------------------------------- /exercises/03.compound-components/01.problem.context/app.tsx: -------------------------------------------------------------------------------- 1 | import { Toggle, ToggleOn, ToggleOff, ToggleButton } from './toggle.tsx' 2 | 3 | export function App() { 4 | return ( 5 |
6 | 7 | The button is on 8 | The button is off 9 | 10 | 11 |
12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /exercises/03.compound-components/01.solution.context/app.tsx: -------------------------------------------------------------------------------- 1 | import { Toggle, ToggleOn, ToggleOff, ToggleButton } from './toggle.tsx' 2 | 3 | export function App() { 4 | return ( 5 |
6 | 7 | The button is on 8 | The button is off 9 | 10 | 11 |
12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /exercises/03.compound-components/02.problem.validation/app.tsx: -------------------------------------------------------------------------------- 1 | import { Toggle, ToggleButton, ToggleOff, ToggleOn } from './toggle.tsx' 2 | 3 | export function App() { 4 | return ( 5 |
6 | 7 | The button is on 8 | The button is off 9 | 10 | 11 |
12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /exercises/03.compound-components/02.solution.validation/app.tsx: -------------------------------------------------------------------------------- 1 | import { Toggle, ToggleButton, ToggleOff, ToggleOn } from './toggle.tsx' 2 | 3 | export function App() { 4 | return ( 5 |
6 | 7 | The button is on 8 | The button is off 9 | 10 | 11 |
12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Advanced React Patterns", 3 | "name": "Advanced React Patterns", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#1675ff", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /exercises/05.prop-getters/FINISHED.mdx: -------------------------------------------------------------------------------- 1 | # Prop Collections and Getters 2 | 3 | 4 | 5 | 👨‍💼 Great work! This is a highly flexible pattern that gives consumers a great 6 | deal of control over how the component is rendered while also handling common 7 | cases with ease. 8 | -------------------------------------------------------------------------------- /exercises/05.prop-getters/01.problem.collections/toggle.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | export function useToggle() { 4 | const [on, setOn] = useState(false) 5 | const toggle = () => setOn(!on) 6 | 7 | // 🐨 Add a property called `togglerProps`. It should be an object that has 8 | // `aria-checked` and `onClick` properties. 9 | return { on, toggle, togglerProps: {} } 10 | } 11 | -------------------------------------------------------------------------------- /exercises/08.control-props/FINISHED.mdx: -------------------------------------------------------------------------------- 1 | # Control Props 2 | 3 | 4 | 5 | 👨‍💼 Great work! You now know how you can allow developers to control your 6 | internal state by using control props. What makes this tricky is supporting both 7 | controlled and uncontrolled and you managed it. Good job! 8 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import defaultConfig from '@epic-web/config/eslint' 2 | 3 | /** @type {import("eslint").Linter.Config} */ 4 | export default [ 5 | { ignores: ['**/babel-standalone.js'] }, 6 | ...defaultConfig, 7 | { 8 | rules: { 9 | // we leave unused vars around for the exercises 10 | 'no-unused-vars': 'off', 11 | '@typescript-eslint/no-unused-vars': 'off', 12 | }, 13 | }, 14 | ] 15 | -------------------------------------------------------------------------------- /exercises/06.state-initializers/01.solution.initial/app.tsx: -------------------------------------------------------------------------------- 1 | import { Switch } from '#shared/switch.tsx' 2 | import { useToggle } from './toggle.tsx' 3 | 4 | export function App() { 5 | const { on, getTogglerProps, reset } = useToggle({ initialOn: true }) 6 | return ( 7 |
8 | 9 |
10 | 11 |
12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /epicshop/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "scripts": { 4 | "start": "cd .. && npm run start", 5 | "test": "playwright test" 6 | }, 7 | "dependencies": { 8 | "@epic-web/config": "^1.16.3", 9 | "@epic-web/workshop-app": "^6.47.1", 10 | "@epic-web/workshop-utils": "^6.47.1", 11 | "@playwright/test": "^1.49.0", 12 | "cross-env": "^7.0.3", 13 | "epicshop": "^6.47.1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /exercises/05.prop-getters/01.problem.collections/app.tsx: -------------------------------------------------------------------------------- 1 | import { Switch } from '#shared/switch.tsx' 2 | import { useToggle } from './toggle.tsx' 3 | 4 | export function App() { 5 | const { on, togglerProps } = useToggle() 6 | return ( 7 |
8 | 9 |
10 | 13 |
14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /exercises/05.prop-getters/01.solution.collections/app.tsx: -------------------------------------------------------------------------------- 1 | import { Switch } from '#shared/switch.tsx' 2 | import { useToggle } from './toggle.tsx' 3 | 4 | export function App() { 5 | const { on, togglerProps } = useToggle() 6 | return ( 7 |
8 | 9 |
10 | 13 |
14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /exercises/05.prop-getters/02.solution.getters/README.mdx: -------------------------------------------------------------------------------- 1 | # Prop Getters 2 | 3 | 4 | 5 | 👨‍💼 Great, now users don't have to worry about whether they're overriding us or 6 | we're overriding them and everything can be an implementation detail which we 7 | can change as needed without worrying about breaking people's expectations. 8 | Composition FTW! 9 | -------------------------------------------------------------------------------- /exercises/07.state-reducer/01.solution.reducer/README.mdx: -------------------------------------------------------------------------------- 1 | # State Reducer 2 | 3 | 4 | 5 | 👨‍💼 This is a powerful example of inversion of control that allows users to 6 | overwrite our entire reducer. But it could be exhausting to users to have to 7 | duplicate most of our reducer just to change a few things. So let's address that 8 | common scenario next. 9 | -------------------------------------------------------------------------------- /exercises/03.compound-components/01.solution.context/README.mdx: -------------------------------------------------------------------------------- 1 | # Compound Components 2 | 3 | 4 | 5 | 👨‍💼 This is an extremely powerful feature that gives us nice and declarative APIs 6 | for our reusable components. However, it's possible people could be using it 7 | wrong so let's explore a way to help people avoid using our API incorrectly 8 | next. 9 | -------------------------------------------------------------------------------- /exercises/08.control-props/01.solution.control/README.mdx: -------------------------------------------------------------------------------- 1 | # Control Props 2 | 3 | 4 | 5 | 👨‍💼 Phew! That was a tough one! But it's extremely powerful. It wouldn't be hard 6 | if we just wanted to lift the state. What makes it hard is wanting to have a 7 | single component that can not only manage its own state, but also have that 8 | state be managed externally as well. 9 | -------------------------------------------------------------------------------- /exercises/04.slots/01.solution.context/text-field.tsx: -------------------------------------------------------------------------------- 1 | import { useId } from 'react' 2 | import { SlotContext } from './slots' 3 | 4 | export function TextField({ 5 | id, 6 | children, 7 | }: { 8 | id?: string 9 | children: React.ReactNode 10 | }) { 11 | const generatedId = useId() 12 | id ??= generatedId 13 | 14 | const slots = { 15 | label: { htmlFor: id }, 16 | input: { id }, 17 | } 18 | 19 | return {children} 20 | } 21 | -------------------------------------------------------------------------------- /exercises/04.slots/02.problem.generic/text-field.tsx: -------------------------------------------------------------------------------- 1 | import { useId } from 'react' 2 | import { SlotContext } from './slots' 3 | 4 | export function TextField({ 5 | id, 6 | children, 7 | }: { 8 | id?: string 9 | children: React.ReactNode 10 | }) { 11 | const generatedId = useId() 12 | id ??= generatedId 13 | 14 | const slots = { 15 | label: { htmlFor: id }, 16 | input: { id }, 17 | } 18 | 19 | return {children} 20 | } 21 | -------------------------------------------------------------------------------- /exercises/04.slots/02.solution.generic/text-field.tsx: -------------------------------------------------------------------------------- 1 | import { useId } from 'react' 2 | import { SlotContext } from './slots' 3 | 4 | export function TextField({ 5 | id, 6 | children, 7 | }: { 8 | id?: string 9 | children: React.ReactNode 10 | }) { 11 | const generatedId = useId() 12 | id ??= generatedId 13 | 14 | const slots = { 15 | label: { htmlFor: id }, 16 | input: { id }, 17 | } 18 | 19 | return {children} 20 | } 21 | -------------------------------------------------------------------------------- /exercises/04.slots/03.problem.prop/text-field.tsx: -------------------------------------------------------------------------------- 1 | import { useId } from 'react' 2 | import { SlotContext } from './slots' 3 | 4 | export function TextField({ 5 | id, 6 | children, 7 | }: { 8 | id?: string 9 | children: React.ReactNode 10 | }) { 11 | const generatedId = useId() 12 | id ??= generatedId 13 | 14 | const slots = { 15 | label: { htmlFor: id }, 16 | input: { id }, 17 | } 18 | 19 | return {children} 20 | } 21 | -------------------------------------------------------------------------------- /exercises/04.slots/03.solution.prop/text-field.tsx: -------------------------------------------------------------------------------- 1 | import { useId } from 'react' 2 | import { SlotContext } from './slots' 3 | 4 | export function TextField({ 5 | id, 6 | children, 7 | }: { 8 | id?: string 9 | children: React.ReactNode 10 | }) { 11 | const generatedId = useId() 12 | id ??= generatedId 13 | 14 | const slots = { 15 | label: { htmlFor: id }, 16 | input: { id }, 17 | } 18 | 19 | return {children} 20 | } 21 | -------------------------------------------------------------------------------- /exercises/03.compound-components/FINISHED.mdx: -------------------------------------------------------------------------------- 1 | # Compound Components 2 | 3 | 4 | 5 | 👨‍💼 Great work! You now know how to create one of the best composition APIs for 6 | UI component libraries. The vast majority of component libraries employ this 7 | pattern and even if you don't build your own, you'll be much better able to use 8 | these libraries because you understand how they work. Good job! 9 | -------------------------------------------------------------------------------- /exercises/08.control-props/01.problem.control/README.mdx: -------------------------------------------------------------------------------- 1 | # Control Props 2 | 3 | 4 | 5 | 👨‍💼 In this exercise, we've created a `` component which can accept a 6 | prop called `on` and another called `onChange`. These work similar to the 7 | `value` and `onChange` props of ``. Your job is to make those props 8 | actually control the state of `on` and call the `onChange` with the suggested 9 | changes. 10 | -------------------------------------------------------------------------------- /exercises/01.composition/01.solution.compose/README.mdx: -------------------------------------------------------------------------------- 1 | # Composition and Layout Components 2 | 3 | 4 | 5 | 👨‍💼 In this one we didn't actually change any visual behavior (the test was 6 | passing before your changes). But we hopefully demonstrated how restructuring 7 | your components can make it easier to maintain and help you avoid the prop 8 | drilling problem and reduce the amount you feel you need to use context. 9 | -------------------------------------------------------------------------------- /exercises/03.compound-components/02.solution.validation/README.mdx: -------------------------------------------------------------------------------- 1 | # Compound Components Validation 2 | 3 | 4 | 5 | 👨‍💼 Runtime validation isn't the best (it would be better if we could enforce 6 | this statically via TypeScript), but unfortunately it's the best we can do with 7 | the composition model offered by React. That said, it's unlikely people will 8 | mess this up now that we have this runtime validation in place. 9 | -------------------------------------------------------------------------------- /exercises/04.slots/01.problem.context/text-field.tsx: -------------------------------------------------------------------------------- 1 | import { useId } from 'react' 2 | 3 | export function TextField({ 4 | id, 5 | children, 6 | }: { 7 | id?: string 8 | children: React.ReactNode 9 | }) { 10 | const generatedId = useId() 11 | id ??= generatedId 12 | 13 | // 🐨 create a slots object that has props for both label and input slots 14 | // 💰 the label should provide an htmlFor prop and the input should provide an id 15 | 16 | // 🐨 wrap this in a SlotContext with the value set to the slots object 17 | return children 18 | } 19 | -------------------------------------------------------------------------------- /exercises/02.latest-ref/01.solution.ref/README.mdx: -------------------------------------------------------------------------------- 1 | # Latest Ref 2 | 3 | 4 | 5 | 👨‍💼 Great work. If you've got a bit more time and you haven't already read this 6 | post, I suggest you give these a read: 7 | 8 | - [How React Uses Closures to Avoid Bugs](https://epicreact.dev/how-react-uses-closures-to-avoid-bugs) 9 | - [The Latest Ref Pattern in React](https://epicreact.dev/the-latest-ref-pattern-in-react) 10 | 11 | With great power comes great responsibility. 12 | -------------------------------------------------------------------------------- /exercises/04.slots/03.solution.prop/README.mdx: -------------------------------------------------------------------------------- 1 | # Slot Prop 2 | 3 | 4 | 5 | 👨‍💼 Great! Now we can use the same component for multiple slots! 6 | 7 | One thing to note is that there's not a great way to have good type safety on 8 | these props, though if you're creative, you could add nice runtime errors, 9 | [for example](https://github.com/adobe/react-spectrum/blob/3db98d2d378f977a88d94e9f2501feca8ef8ce51/packages/react-aria-components/src/utils.tsx#L169-L181). 10 | -------------------------------------------------------------------------------- /exercises/06.state-initializers/FINISHED.mdx: -------------------------------------------------------------------------------- 1 | # State Initializer 2 | 3 | 4 | 5 | 👨‍💼 Great job! You now know what to do to properly handle the initialization (and 6 | reset) of your state. 7 | 8 | 🦉 Keep in mind that the `key` prop can also be used as a way to reset the state 9 | of a component, and it works pretty well. However it does require that 10 | everything is unmounted and remounted, which may not be what you want depending 11 | on the situation. 12 | -------------------------------------------------------------------------------- /exercises/05.prop-getters/02.solution.getters/app.tsx: -------------------------------------------------------------------------------- 1 | import { Switch } from '#shared/switch.tsx' 2 | import { useToggle } from './toggle.tsx' 3 | 4 | export function App() { 5 | const { on, getTogglerProps } = useToggle() 6 | return ( 7 |
8 | 9 |
10 | 19 |
20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /exercises/06.state-initializers/01.problem.initial/app.tsx: -------------------------------------------------------------------------------- 1 | import { Switch } from '#shared/switch.tsx' 2 | import { useToggle } from './toggle.tsx' 3 | 4 | export function App() { 5 | // 🐨 add an initialOn option (set it to true) and get the reset callback from useToggle 6 | const { on, getTogglerProps } = useToggle() 7 | // 💣 delete this reset callback in favor of what you get from useToggle 8 | const reset = () => {} 9 | return ( 10 |
11 | 12 |
13 | 14 |
15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /exercises/06.state-initializers/02.problem.stability/app.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { Switch } from '#shared/switch.tsx' 3 | import { useToggle } from './toggle.tsx' 4 | 5 | export function App() { 6 | const [initialOn, setInitialOn] = useState(true) 7 | const { on, getTogglerProps, reset } = useToggle({ initialOn }) 8 | return ( 9 |
10 | 13 | 14 |
15 | 16 |
17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /exercises/06.state-initializers/02.solution.stability/app.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { Switch } from '#shared/switch.tsx' 3 | import { useToggle } from './toggle.tsx' 4 | 5 | export function App() { 6 | const [initialOn, setInitialOn] = useState(true) 7 | const { on, getTogglerProps, reset } = useToggle({ initialOn }) 8 | return ( 9 |
10 | 13 | 14 |
15 | 16 |
17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /exercises/04.slots/03.solution.prop/app.tsx: -------------------------------------------------------------------------------- 1 | import { Input, Label, Switch, Text } from './slots.tsx' 2 | import { TextField } from './text-field.tsx' 3 | import { Toggle } from './toggle.tsx' 4 | 5 | export function App() { 6 | return ( 7 |
8 |
9 | 10 | 11 | 12 | Let's party 🥳 13 | Sad town 😭 14 | 15 |
16 |
17 |
18 | 19 | 20 | 21 | 22 |
23 |
24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /exercises/04.slots/02.solution.generic/app.tsx: -------------------------------------------------------------------------------- 1 | import { Input, Label } from './slots.tsx' 2 | import { TextField } from './text-field.tsx' 3 | import { Toggle, ToggleButton, ToggleOff, ToggleOn } from './toggle.tsx' 4 | 5 | export function App() { 6 | return ( 7 |
8 |
9 | 10 | 11 | 12 | Let's party 🥳 13 | Sad town 😭 14 | 15 |
16 |
17 |
18 | 19 | 20 | 21 | 22 |
23 |
24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /exercises/08.control-props/01.solution.control/uncontrolled.test.tsx: -------------------------------------------------------------------------------- 1 | import { expect, testStep } from '@epic-web/workshop-utils/test' 2 | import { render, screen } from '@testing-library/react' 3 | import { userEvent } from '@testing-library/user-event' 4 | import { Toggle } from './toggle.tsx' 5 | 6 | await testStep('can render Toggle with no props', () => { 7 | render() 8 | }) 9 | 10 | await testStep('toggle still operates like a toggle', async () => { 11 | const toggle = screen.getByRole('switch') 12 | expect(toggle).to.have.attr('aria-checked', 'false') 13 | await userEvent.click(toggle) 14 | expect(toggle).to.have.attr('aria-checked', 'true') 15 | }) 16 | -------------------------------------------------------------------------------- /exercises/06.state-initializers/01.problem.initial/README.mdx: -------------------------------------------------------------------------------- 1 | # Initialize Toggle 2 | 3 | 4 | 5 | 👨‍💼 Our toggle component should be able to be customizable for the initial state 6 | and reset to the initial state. 7 | 8 | 🧝‍♂️ I've updated the toggle component to use a reducer instead of `useState`. If 9 | you'd like to back up, and do that yourself in the playground, be my guest. Or 10 | you can check my work instead. 11 | 12 | 👨‍💼 Please add a case in our reducer for the `reset` logic, and add an option to 13 | our `useToggle` hook for setting the initialOn state. 14 | -------------------------------------------------------------------------------- /exercises/05.prop-getters/02.problem.getters/app.tsx: -------------------------------------------------------------------------------- 1 | import { Switch } from '#shared/switch.tsx' 2 | import { useToggle } from './toggle.tsx' 3 | 4 | export function App() { 5 | // 💣 delete this: 6 | const getTogglerProps = (props: any) => props 7 | // 🐨 destructure the getTogglerProps function from useToggle 8 | const { on } = useToggle() 9 | return ( 10 |
11 | 12 |
13 | 22 |
23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /exercises/04.slots/01.solution.context/app.tsx: -------------------------------------------------------------------------------- 1 | import { useId } from 'react' 2 | import { Input, Label } from './slots.tsx' 3 | import { TextField } from './text-field.tsx' 4 | import { Toggle, ToggleButton, ToggleOff, ToggleOn } from './toggle.tsx' 5 | 6 | export function App() { 7 | const partyModeId = useId() 8 | return ( 9 |
10 |
11 | 12 | 13 | 14 | Let's party 🥳 15 | Sad town 😭 16 | 17 |
18 |
19 |
20 | 21 | 22 | 23 | 24 |
25 |
26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /exercises/05.prop-getters/02.problem.getters/toggle.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | export function useToggle() { 4 | const [on, setOn] = useState(false) 5 | const toggle = () => setOn(!on) 6 | 7 | // 🐨 create a function called getTogglerProps that accepts an object 8 | // of props and returns an object of props that includes 'aria-checked' and onClick. 9 | 10 | // 💰 Make sure to handle the case where the user provides their own 11 | // 'aria-checked' and 'onClick' props (as well as if they don't or if they 12 | // provide more props). 13 | 14 | return { 15 | on, 16 | toggle, 17 | // 🐨 swap togglerProps with getTogglerProps 18 | togglerProps: { 19 | 'aria-checked': on, 20 | onClick: toggle, 21 | }, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /exercises/05.prop-getters/02.solution.getters/toggle.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | function callAll>( 4 | ...fns: Array<((...args: Args) => unknown) | undefined> 5 | ) { 6 | return (...args: Args) => fns.forEach(fn => fn?.(...args)) 7 | } 8 | 9 | export function useToggle() { 10 | const [on, setOn] = useState(false) 11 | const toggle = () => setOn(!on) 12 | 13 | function getTogglerProps({ 14 | onClick, 15 | ...props 16 | }: { 17 | onClick?: React.ComponentProps<'button'>['onClick'] 18 | } & Props) { 19 | return { 20 | 'aria-checked': on, 21 | onClick: callAll(onClick, toggle), 22 | ...props, 23 | } 24 | } 25 | 26 | return { 27 | on, 28 | toggle, 29 | getTogglerProps, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /exercises/07.state-reducer/02.solution.default/README.mdx: -------------------------------------------------------------------------------- 1 | # Default State Reducer 2 | 3 | 4 | 5 | 👨‍💼 It's a pretty simple, yet powerful solution. Inversion of control with good 6 | defaults allows our component users to have a lot more power and will reduce the 7 | amount of feature requests you get for these components by a lot, I promise. 8 | 9 | 🦉 An important thing to note is that once you do this your internal state 10 | becomes a part of your public API. So if you change the name of an internal 11 | state variable, then that will be a breaking change for your users. So be 12 | careful with this. 13 | 14 | 🕷 With great power comes great responsibility. 15 | -------------------------------------------------------------------------------- /exercises/04.slots/03.problem.prop/app.tsx: -------------------------------------------------------------------------------- 1 | import { Input, Label } from './slots.tsx' 2 | import { TextField } from './text-field.tsx' 3 | import { Toggle, ToggleButton, ToggleOff, ToggleOn } from './toggle.tsx' 4 | 5 | export function App() { 6 | return ( 7 |
8 |
9 | 10 | 11 | {/* 🐨 switch this for the Switch slot component */} 12 | 13 | {/* 🐨 change these to the Text slot component with appropriate slot props */} 14 | Let's party 🥳 15 | Sad town 😭 16 | 17 |
18 |
19 |
20 | 21 | 22 | 23 | 24 |
25 |
26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /exercises/07.state-reducer/02.problem.default/README.mdx: -------------------------------------------------------------------------------- 1 | # Default State Reducer 2 | 3 | 4 | 5 | 👨‍💼 Our `toggleReducer` is pretty simple, so it's not a huge pain for people to 6 | implement their own. However, in a more realistic scenario, people may struggle 7 | with having to basically re-implement our entire reducer which could be pretty 8 | complex. 9 | 10 | So for the this step, we're going to `export` the default reducer so people can 11 | use that inside their own reducers as needed. 12 | 13 | Go ahead and do this by changing the `toggleStateReducer` function inside the 14 | `` example to use the default reducer instead of having to re-implement 15 | what to do when the action type is `'reset'`. 16 | -------------------------------------------------------------------------------- /epicshop/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:24-bookworm-slim as base 2 | 3 | RUN apt-get update && apt-get install -y git 4 | 5 | ENV EPICSHOP_GITHUB_REPO=https://github.com/epicweb-dev/advanced-react-patterns 6 | ENV EPICSHOP_CONTEXT_CWD="/myapp/workshop-content" 7 | ENV EPICSHOP_HOME_DIR="/myapp/.epicshop" 8 | ENV EPICSHOP_DEPLOYED="true" 9 | ENV EPICSHOP_DISABLE_WATCHER="true" 10 | ENV FLY="true" 11 | ENV PORT="8080" 12 | ENV NODE_ENV="production" 13 | 14 | WORKDIR /myapp 15 | 16 | # Clone the workshop repo during build time, excluding database files 17 | RUN git clone --depth 1 ${EPICSHOP_GITHUB_REPO} ${EPICSHOP_CONTEXT_CWD} 18 | 19 | ADD . . 20 | 21 | RUN npm install --omit=dev 22 | 23 | RUN cd ${EPICSHOP_CONTEXT_CWD} && \ 24 | npx epicshop warm 25 | 26 | CMD cd ${EPICSHOP_CONTEXT_CWD} && \ 27 | npx epicshop start 28 | -------------------------------------------------------------------------------- /exercises/01.composition/01.problem.compose/README.mdx: -------------------------------------------------------------------------------- 1 | # Composition and Layout Components 2 | 3 | 4 | 5 | 👨‍💼 In this exercise we've got a simple user interface with several components 6 | necessitating passing state and updaters around. We're going to restructure 7 | things so we pass react elements instead of state and updaters. We might be 8 | going a _tiny_ bit overboard, but the goal is for this to be instructive for 9 | you. 10 | 11 | By the time you're done, the whole UI should look and function exactly as 12 | before, but you'll get a sense for how to use this pattern. The tests will be 13 | there just for you to verify you haven't broken anything that should be working 14 | if you want to use them. 15 | -------------------------------------------------------------------------------- /exercises/04.slots/03.solution.prop/toggle.tsx: -------------------------------------------------------------------------------- 1 | import { useId, useState } from 'react' 2 | import { SlotContext } from './slots' 3 | 4 | export function Toggle({ 5 | id, 6 | children, 7 | }: { 8 | id?: string 9 | children: React.ReactNode 10 | }) { 11 | const [on, setOn] = useState(false) 12 | const generatedId = useId() 13 | id ??= generatedId 14 | 15 | const toggle = () => setOn(!on) 16 | 17 | const labelProps = { htmlFor: id } 18 | const onTextProps = { hidden: on ? undefined : true } 19 | const offTextProps = { hidden: on ? true : undefined } 20 | const switchProps = { id, on, onClick: toggle } 21 | 22 | return ( 23 | 31 | {children} 32 | 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /exercises/04.slots/02.problem.generic/slots.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, use } from 'react' 2 | 3 | type Slots = Record> 4 | export const SlotContext = createContext({}) 5 | 6 | function useSlotProps(props: Props, slot: string): Props { 7 | const slots = use(SlotContext) 8 | 9 | // a more proper "mergeProps" function is in order here 10 | // to handle things like merging event handlers better. 11 | // we'll get to that a bit in a later exercise. 12 | return { ...slots[slot], slot, ...props } as Props 13 | } 14 | 15 | export function Label(props: React.ComponentProps<'label'>) { 16 | props = useSlotProps(props, 'label') 17 | return