├── next.config.js ├── public ├── favicon.ico ├── devanywhere-logo.png └── vercel.svg ├── README.md ├── pages ├── _app.js ├── index.js ├── api │ └── hello-world.js └── pyramid-of-doom.js ├── src ├── components │ └── welcome-view.js ├── server │ ├── hocs │ │ ├── with-auth.js │ │ ├── with-request-id.js │ │ └── with-logger.js │ └── lib │ │ ├── default-middleware.js │ │ └── create-route.js ├── test.js └── hocs │ ├── with-features.js │ ├── with-user.js │ ├── with-logger.js │ ├── with-providers.js │ └── with-layout.js ├── .eslintrc.json ├── .babelrc ├── styles ├── globals.css └── Home.module.css ├── .gitignore ├── package.json └── LICENSE /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | reactStrictMode: true, 3 | }; 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paralleldrive/react-component-composition/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Component Composition 2 | 3 | Example repo demonstrating composition patterns for React and Next.js Apps 4 | -------------------------------------------------------------------------------- /public/devanywhere-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paralleldrive/react-component-composition/HEAD/public/devanywhere-logo.png -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import "../styles/globals.css"; 2 | 3 | function MyApp({ Component, pageProps }) { 4 | return ; 5 | } 6 | 7 | export default MyApp; 8 | -------------------------------------------------------------------------------- /src/components/welcome-view.js: -------------------------------------------------------------------------------- 1 | const WelcomeView = ({ userName = "" } = {}) => { 2 | return

Welcome {userName}!

; 3 | }; 4 | 5 | export default WelcomeView; 6 | -------------------------------------------------------------------------------- /src/server/hocs/with-auth.js: -------------------------------------------------------------------------------- 1 | const withAuth = async ({ request, response }) => { 2 | console.log("User is authenticated!"); 3 | 4 | return { request, response }; 5 | }; 6 | 7 | export default withAuth; 8 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "next/core-web-vitals", 4 | "plugin:prettier/recommended" 5 | ], 6 | "parserOptions": { 7 | "sourceType": "module", 8 | "ecmaVersion": 2022 9 | } 10 | } -------------------------------------------------------------------------------- /src/test.js: -------------------------------------------------------------------------------- 1 | import { describe } from "riteway"; 2 | 3 | describe("tests", async (assert) => { 4 | assert({ 5 | given: "nothing", 6 | should: "run tests", 7 | actual: true, 8 | expected: true, 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/hocs/with-features.js: -------------------------------------------------------------------------------- 1 | const withFeatures = (Component) => { 2 | const features = ["landing-page"]; 3 | 4 | return function WithFeatures(props) { 5 | return ; 6 | }; 7 | }; 8 | 9 | export default withFeatures; 10 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import withProviders from "../src/hocs/with-providers"; 3 | import WelcomeView from "../src/components/welcome-view"; 4 | 5 | function Home({ user = {} } = {}) { 6 | return ; 7 | } 8 | 9 | export default withProviders()(Home); 10 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "development": { 4 | "presets": [ 5 | "next/babel" 6 | ] 7 | }, 8 | "production": { 9 | "presets": [ 10 | "next/babel" 11 | ] 12 | }, 13 | "test": { 14 | "presets": [ 15 | "@babel/env", 16 | "@babel/react" 17 | ] 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /src/server/lib/default-middleware.js: -------------------------------------------------------------------------------- 1 | import { asyncPipe } from "./create-route"; 2 | 3 | import withAuth from "../hocs/with-auth"; 4 | import withRequestId from "../hocs/with-request-id"; 5 | import withLogger from "../hocs/with-logger"; 6 | 7 | const defaultMiddleware = asyncPipe(withAuth, withRequestId, withLogger); 8 | 9 | export default defaultMiddleware; 10 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | -------------------------------------------------------------------------------- /pages/api/hello-world.js: -------------------------------------------------------------------------------- 1 | import createRoute from "../../src/server/lib/create-route"; 2 | import defaultMiddleware from "../../src/server/lib/default-middleware"; 3 | 4 | const helloWorld = async ({ request, response }) => { 5 | response.status(200); 6 | response.json({ message: "Hello World" }); 7 | }; 8 | 9 | export default createRoute(defaultMiddleware, helloWorld); 10 | -------------------------------------------------------------------------------- /src/server/hocs/with-request-id.js: -------------------------------------------------------------------------------- 1 | import cuid from "cuid"; 2 | 3 | const appendId = (response) => { 4 | if (!response.locals) response.locals = {}; 5 | response.locals.requestId = cuid(); 6 | return response; 7 | }; 8 | 9 | const withRequestId = async ({ request, response }) => ({ 10 | request, 11 | response: appendId(response), 12 | }); 13 | 14 | export default withRequestId; 15 | -------------------------------------------------------------------------------- /src/hocs/with-user.js: -------------------------------------------------------------------------------- 1 | import cuid from "cuid"; 2 | 3 | export const createUser = ({ 4 | id = cuid(), 5 | name = "Anon", 6 | avatar = "anon.png", 7 | } = {}) => ({ 8 | id, 9 | name, 10 | avatar, 11 | }); 12 | 13 | const withUser = (Component) => { 14 | const user = createUser(); 15 | 16 | return function WithUser(props) { 17 | return ; 18 | }; 19 | }; 20 | 21 | export default withUser; 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /src/hocs/with-logger.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | 3 | export const log = (x) => 4 | console.log({ 5 | timestamp: Date.now(), 6 | ...x, 7 | }); 8 | 9 | const withLogger = (Component) => { 10 | return function LoggingProvider({ ...props }) { 11 | const { user: id } = props; 12 | useEffect(() => { 13 | log({ 14 | type: "mount", 15 | name: "MyPage", 16 | user: id, 17 | }); 18 | }, [id]); 19 | return ; 20 | }; 21 | }; 22 | 23 | export default withLogger; 24 | -------------------------------------------------------------------------------- /src/server/hocs/with-logger.js: -------------------------------------------------------------------------------- 1 | const withLogger = async ({ request, response }) => { 2 | const { body, headers, method, query, url } = request; 3 | const requestId = response.locals.requestId; 4 | 5 | console.log(`${requestId} REQUEST MADE`); 6 | console.log({ 7 | requestId, 8 | time: new Date().toISOString(), 9 | url, 10 | method, 11 | headers: JSON.stringify(headers), 12 | body: JSON.stringify(body), 13 | query: JSON.stringify(query), 14 | }); 15 | 16 | return { request, response }; 17 | }; 18 | 19 | export default withLogger; 20 | -------------------------------------------------------------------------------- /src/hocs/with-providers.js: -------------------------------------------------------------------------------- 1 | import withFeatures from "./with-features.js"; 2 | import withLayout from "./with-layout.js"; 3 | import withLogger from "./with-logger.js"; 4 | import withUser from "./with-user.js"; 5 | 6 | const compose = 7 | (...fns) => 8 | (x) => 9 | fns.reduceRight((y, f) => f(y), x); 10 | 11 | const withProviders = ({ 12 | showBorderDecorations = true, 13 | showFooter = true, 14 | } = {}) => 15 | compose( 16 | withUser, 17 | withFeatures, 18 | withLogger, 19 | withLayout({ showBorderDecorations, showFooter }) 20 | ); 21 | 22 | export default withProviders; 23 | -------------------------------------------------------------------------------- /src/server/lib/create-route.js: -------------------------------------------------------------------------------- 1 | export const asyncPipe = 2 | (...fns) => 3 | (x) => 4 | fns.reduce(async (y, f) => f(await y), x); 5 | 6 | const createRoute = 7 | (...middleware) => 8 | async (request, response) => { 9 | try { 10 | await asyncPipe(...middleware)({ 11 | request, 12 | response, 13 | }); 14 | } catch (e) { 15 | const requestId = response.locals.requestId; 16 | const { url, method, headers } = request; 17 | console.log({ 18 | time: new Date().toISOString(), 19 | body: JSON.stringify(request.body), 20 | query: JSON.stringify(request.query), 21 | method, 22 | headers: JSON.stringify(headers), 23 | error: true, 24 | url, 25 | message: e.message, 26 | stack: e.stack, 27 | requestId, 28 | }); 29 | response.status(500); 30 | response.json({ 31 | error: "Internal Server Error", 32 | requestId, 33 | }); 34 | } 35 | }; 36 | 37 | export default createRoute; 38 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-component-composition", 3 | "private": true, 4 | "scripts": { 5 | "dev": "next dev", 6 | "build": "next build", 7 | "start": "next start", 8 | "lint": "eslint --fix . && echo 'Lint complete.'", 9 | "test": "NODE_ENV=test node -r @babel/register -r @babel/core src/test | tap-nirvana", 10 | "watch": "watch \"clear && npm run -s test && npm run -s lint\" src" 11 | }, 12 | "dependencies": { 13 | "cuid": "2.1.8", 14 | "next": "12.3.1", 15 | "react": "18.2.0", 16 | "react-dom": "18.2.0" 17 | }, 18 | "devDependencies": { 19 | "@babel/core": "7.19.3", 20 | "@babel/preset-env": "7.19.3", 21 | "@babel/preset-react": "7.18.6", 22 | "@babel/register": "7.18.9", 23 | "eslint": "8.24.0", 24 | "eslint-config-next": "12.3.1", 25 | "eslint-config-prettier": "8.5.0", 26 | "eslint-plugin-prettier": "4.2.1", 27 | "eslint-plugin-react": "7.31.8", 28 | "eslint-plugin-react-hooks": "4.6.0", 29 | "prettier": "2.7.1", 30 | "riteway": "6.3.1", 31 | "tap-nirvana": "1.1.0", 32 | "watch": "1.0.2" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Parallel Drive 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /pages/pyramid-of-doom.js: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, useEffect } from "react"; 2 | import { BorderDecorations, Footer } from "../src/hocs/with-layout"; 3 | import { createUser } from "../src/hocs/with-user"; 4 | import { log } from "../src/hocs/with-logger"; 5 | import WelcomeView from "../src/components/welcome-view"; 6 | 7 | const userContext = createContext(); 8 | const UserProvider = userContext.Provider; 9 | 10 | const featureContext = createContext(); 11 | const FeatureProvider = featureContext.Provider; 12 | const features = ["landing-page"]; 13 | 14 | const PageComponent = () => { 15 | const user = useContext(userContext); 16 | const features = useContext(featureContext); 17 | 18 | useEffect(() => { 19 | log({ 20 | type: "mount", 21 | name: "MyPage", 22 | user: user.id, 23 | }); 24 | }, [user]); 25 | 26 | return ( 27 | <> 28 | 29 | 30 |