├── .github
└── workflows
├── .gitignore
├── LICENSE
├── README.md
└── images
└── trek_tweet.png
/.github/workflows:
--------------------------------------------------------------------------------
1 | name: Test deployment
2 |
3 | on:
4 | push:
5 | branches:
6 | - 'master'
7 |
8 | env:
9 | CI: true
10 | NODE_OPTIONS: --max-old-space-size=16384
11 |
12 | jobs:
13 |
14 | dummy-deploy:
15 | name: dummy-deploy
16 | runs-on: ubuntu-latest
17 | steps:
18 | - uses: actions/checkout@v2
19 | - uses: actions/setup-node@v1
20 | with:
21 | node-version: '14'
22 |
23 | - name: start deployment
24 | uses: Tallyb/deployments@master
25 | id: deployment
26 | with:
27 | step: start
28 | token: ${{ secrets.GITHUB_TOKEN }}
29 | env: my-mockup
30 | ref: ${{github.sha}}
31 | transient: true
32 |
33 | - name: update deployment status
34 | uses: Tallyb/deployments@master
35 | if: always()
36 | with:
37 | step: finish
38 | token: ${{ secrets.GITHUB_TOKEN }}
39 | status: ${{ job.status }}
40 | desc: my mockup
41 | env_url: https://google.com
42 | deployment_id: ${{ steps.deployment.outputs.deployment_id }}
43 |
44 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # TypeScript v1 declaration files
45 | typings/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Microbundle cache
57 | .rpt2_cache/
58 | .rts2_cache_cjs/
59 | .rts2_cache_es/
60 | .rts2_cache_umd/
61 |
62 | # Optional REPL history
63 | .node_repl_history
64 |
65 | # Output of 'npm pack'
66 | *.tgz
67 |
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
71 | # dotenv environment variables file
72 | .env
73 | .env.test
74 |
75 | # parcel-bundler cache (https://parceljs.org/)
76 | .cache
77 |
78 | # Next.js build output
79 | .next
80 |
81 | # Nuxt.js build / generate output
82 | .nuxt
83 | dist
84 |
85 | # Gatsby files
86 | .cache/
87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
88 | # https://nextjs.org/blog/next-9-1#public-directory-support
89 | # public
90 |
91 | # vuepress build output
92 | .vuepress/dist
93 |
94 | # Serverless directories
95 | .serverless/
96 |
97 | # FuseBox cache
98 | .fusebox/
99 |
100 | # DynamoDB Local files
101 | .dynamodb/
102 |
103 | # TernJS port file
104 | .tern-port
105 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Tally Barak
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Reusable Components Styleguide
2 |
3 | __Tips and tricks for making components shareable across different projects (framework agnostic).__
4 |
5 | Components are now the most popular method of developing frontend applications. The popular frameworks and browsers themselves support splitting applications to individual components.
6 | Components let you split your UI into independent, reusable pieces, and think about each piece in isolation.
7 |
8 | 
9 |
10 | **Why me?**
11 |
12 | This series summarizes what I have learned in the last months working as head of Developer Experience at Bit. Bit is a components collaboration tool that helps developers build components in different applications and repositories and share them.
13 | During this period, I have seen components developed by many different teams and across different frameworks. The good parts and the bad parts have led me to define some guidelines that can help people build more independent, isolated, and hence reusable components.
14 |
15 | ## Table of Contents
16 |
17 | - [Reusable Components Styleguide](#reusable-components-styleguide)
18 | - [Table of Contents](#table-of-contents)
19 | - [Directory Structure](#directory-structure)
20 | - [One component -> One directory](#one-component---one-directory)
21 | - [Use Aliases](#use-aliases)
22 | - [APIs](#apis)
23 | - [Use discrete values](#use-discrete-values)
24 | - [Set Defaults](#set-defaults)
25 | - [Globals](#globals)
26 | - [Do not rely on global variables](#do-not-rely-on-global-variables)
27 | - [Provide fallbacks to globals](#provide-fallbacks-to-globals)
28 | - [NPM Packages](#npm-packages)
29 | - [Ensure versions compatibility](#ensure-versions-compatibility)
30 | - [Minimize packages](#minimize-packages)
31 | - [Styling](#styling)
32 | - [Scope styles to component](#scope-styles-to-component)
33 | - [Restrict styles with themes](#restrict-styles-with-themes)
34 | - [CSS Variables as theming variables](#css-variables-as-theming-variables)
35 | - [State Management](#state-management)
36 | - [Decouple data and layout](#decouple-data-and-layout)
37 |
38 | ## Directory Structure
39 |
40 | ### One component -> One directory
41 |
42 | ✅ _Do_: Put all the component related files in a single directory, including component's code, stylings, tests, documentation, storybook stories, and testing snapshots. If your components is using sub-components, i.e., components that you can only use within the context of the parent component (List and ListItem or a component and its styled component), it make sense to include them in the same directory. Component consumer is getting all the components packaged together.
43 |
44 | ```bash
45 | ├── Button.spec.tsx
46 | ├── Button.stories.tsx
47 | ├── Button.style.tsx
48 | ├── Button.tsx
49 | └── __snapshots__
50 | └── Button.spec.tsx.snap
51 |
52 | 1 directory, 5 files
53 | ```
54 |
55 | ❌ _Avoid_: Placing files according to their type in different directories.
56 |
57 | ```bash
58 | ├── components
59 | │ ├── Button.style.tsx
60 | │ └── Button.tsx
61 | ├── stories
62 | │ └── Button.stories.tsx
63 | └── tests
64 | ├── Button.spec.tsx
65 | └── __snapshots__
66 | └── Button.spec.tsx.snap
67 |
68 | 4 directories, 5 files
69 | ```
70 |
71 | ❔**Why?**
72 | The component produces creates a directory structure that is easily consumable by placing all the related files together. Having all files located in a single directory makes it easier for component consumers to reason about the items that are interconnected. File references are becoming shorter and easier to find the referenced item. Shorter file references make it easy to move the component to different directories. A typical reference pattern is:
73 | style <- code <- story <- test
74 | The component code is importing the component's style (CSS or JS in CSS style). A story (supporting CSF format) is importing the code to build the story. And the test is importing and instancing a story to validate functionality. (It is also totally ok for the test is directly importing the code).
75 |
76 | ### Use Aliases
77 |
78 | ✅ _Do_: Reference other components using aliases.
79 |
80 | ```js
81 | import { } from '@utils'
82 | ```
83 |
84 | ❌ _Avoid_: using relative pathname to other components.
85 |
86 | ```js
87 | import { } from '../../../src/utils';
88 | ```
89 |
90 | ❔**Why?**
91 | Having a path like the above makes it hard to move the component files around in our project, as we need to keep the reference valid. Using backward relative paths couples the component to the specific file structure of the project and forces the consuming project to use a similar structure.
92 | Webpack, Rollup, and Typescript provide methods for using fixed references instead of relative ones. Typescript uses the `paths` mapping to create a mapping to reference components. Rollup uses the `@rollup/plugin-alias` for creating similar aliases, and Webpack is using the setting of `resolve.alias` for the same purpose.
93 |
94 | ## APIs
95 |
96 | Component's APIs are the data attributes it receives and the callbacks it exposes. The general rule is to try and minimize the APIs surface to the necessary minimum. For component producers, this means to prepare the APIs so they are logical and consistent. Component consumers get APIs that are simple to use and reduces the learning curve when using the component.
97 |
98 | ### Use discrete values
99 |
100 | ✅ _Do_: Use discrete values such as Enums or string literals for requiring specific options.
101 |
102 | ```ts
103 | type LocationProps = {
104 | position: 'TopLeft' | 'TopRight' | 'BottomLeft' | 'BottomRight'
105 | }
106 | ```
107 |
108 | ❌ _Avoid_: using multiple booleans
109 |
110 | ```ts
111 | type LocationProps = {
112 | isLeft: boolean,
113 | isTop: boolean,
114 | }
115 | ```
116 |
117 | ❔**Why?**
118 | Interdependencies between parameters make it harder for the consumer to use it. Creating more simplistic params paves a smoother way for developers to consume the components.
119 |
120 | ### Set Defaults
121 |
122 | ✅ _Do_: Set reasonable defaults for most params
123 |
124 | ```ts
125 | type LocationProps = {
126 | position: 'TopLeft' | 'TopRight' | 'BottomLeft' | 'BottomRight'
127 | }
128 |
129 | defaultProps = {
130 | position: 'TopLeft'
131 | }
132 | ```
133 |
134 | ❌ _Avoid_: Making parameters required and expect user to fill in values for all of them.
135 |
136 | ❔**Why?**
137 | Setting parameters makes it easy for the consumer to start using the component, rather than find fair values for all parameters. Once incorporating the component, tweaking it to the exact need is more tranquil.
138 |
139 | ## Globals
140 |
141 | ### Do not rely on global variables
142 |
143 | ✅ _Do_: get globals in the component's APIs instead of accessing a global param
144 |
145 | ```js
146 | export const Card = ({ title, paragraph, someGlobal }: CardProps) =>
147 |
153 | ```
154 |
155 | ❌ _Avoid_: Assuming a value
156 |
157 | Components may rely on globals, such as window.someGlobal, assuming that the global variable already exists.
158 |
159 | ❔**Why?**
160 | Relying on parameters gives the consuming application greater flexibility in using the components and does not require it to adhere to the same structure that exists in the producing application.
161 |
162 | ### Provide fallbacks to globals
163 |
164 | ✅ _Do_: Use reasonable defaults when accessing globals that may not exist
165 |
166 | ```js
167 | if (typeof window.someGlobal === 'function') {
168 | window.someGlobal()
169 | } else {
170 | // do something else or set the global variable and use it
171 | }
172 | ```
173 |
174 | ❌ _Avoid_: accessing global with no safe fallback
175 |
176 | ```js
177 | window.someGlobal()
178 | ```
179 |
180 | ❔**Why?**
181 | Fallbacks let the consuming application a way to build the application in a manner that is less coupled to the way the provider application. It also does not assumes that the global was set at the time it is consumed.
182 |
183 | ## NPM Packages
184 |
185 | Our code relies on third-party libraries for providing specific functionalities, such as scrolling, charting, animations, and more. Third-party libraries are important but take care when adding them.
186 |
187 | ### Ensure versions compatibility
188 |
189 | ✅ _Do_: Define packages that are likely to exist in the consuming app as peerDependency with relaxed versioning.
190 |
191 | "peerDependencies": {
192 | "my-lib": ">=1.0.0"
193 | }
194 |
195 | ❌ _Avoid_: specifying very strict version as dependency
196 |
197 | "dependencies": {
198 | "my-lib": "1.0.0"
199 | }
200 |
201 | ❔**Why?**
202 | To understand the problem, let's understand how package managers resolve dependencies. Assume we have two libraries with the following package.json files:
203 |
204 | ```json
205 | {
206 | "name": "library-a",
207 | "version": "1.0.0",
208 | "dependencies": {
209 | "library-b": "^1.0.0",
210 | "library-c": "^1.0.0"
211 | }
212 | }
213 |
214 | {
215 | "name": "library-b",
216 | "version": "1.0.0",
217 | "dependencies": {
218 | "library-c": "^2.0.0"
219 | }
220 | }
221 | ```
222 |
223 | The resulting node_modules tree will look as follow:
224 |
225 | ```bash
226 | - library-a/
227 | - node_modules/
228 | - library-c/
229 | - package.json <-- library-c@1.0.0
230 | - library-b/
231 | - package.json
232 | - node_modules/
233 | - library-c/
234 | - package.json <-- library-c@2.0.0
235 | ```
236 |
237 | You can see that library-c is installed twice with two separate versions. In some cases, such as with React or Angular frameworks, this can even cause errors. However, if the configuration is kept as follow:
238 |
239 | ```json
240 | {
241 | "name": "library-a",
242 | "version": "1.0.0",
243 | "dependencies": {
244 | "library-b": "^1.0.0",
245 | },
246 | "peerDependencies": {
247 | "library-c": ">=1.0.0"
248 | },
249 | }
250 |
251 | {
252 | "name": "library-b",
253 | "version": "1.0.0",
254 | "dependencies": {
255 | "library-c": "^2.0.0"
256 | }
257 | }
258 | ```
259 |
260 | library-c is only installed once:
261 |
262 | ```bash
263 | - library-a/
264 | - node_modules/
265 | - library-c/
266 | - package.json <-- library-c2@.0.0
267 | - library-b/
268 | - package.json
269 | ```
270 |
271 | Also, make sure the peer dependency has very loose versioning. Why? When installing packages, both NPM and Yarn flatten the dependency tree as much as possible. So let's say we have packages A and B. They both need package C but with different versions — say 1.1.0 and 1.2.0. NPM and Yarn will obey the requirement and install both versions of C under A and B. However, if A and B require C in version ">1.0.0", C is only installed once with the latest version.
272 |
273 | ### Minimize packages
274 |
275 | ✅ _Do_: Revise package.json dependencies often to make sure they are all in use. Prefer language features over packages (e.g. lodash vs. built it functions).
276 |
277 | ❌ _Avoid_: Using functionality duplicated across multiple packages.
278 |
279 | ❔**Why?**
280 | When reusing code, you also need to reuse the packages that use it. Relying on multiple packages makes it hard to move components between applications, but also increase bundle size for all the component consumers.
281 |
282 | ## Styling
283 |
284 | By design, CSS is global and cascading without any module system or encapsulation.
285 |
286 | ### Scope styles to component
287 |
288 | ✅ _Do_: Use a CSS mechanism that scope the style to the component. In React the popular css-in-js frameworks such as [Emotion](https://github.com/emotion-js/emotion), [Styled Components](https://github.com/styled-components/styled-components) and [JSS](https://github.com/cssinjs/jss) are famous. Vue is supporting scoped styled [out of the box](https://vue-loader.vuejs.org/guide/scoped-css.html). Angular also has scoped styles built in with the [viewEncapsulation property](https://angular.io/api/core/ViewEncapsulation). CSS is scoped in web components via the [Shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM).
289 |
290 | ❌ _Avoid_: Rely on application level styles in components.
291 |
292 | ❔**Why?**
293 | When reusing components across different apps the application level styles are likely to change. Relying on global styles can break styling. Encapsulating all style inside the component ensures it looks the same even when transported between applications.
294 |
295 | ### Restrict styles with themes
296 |
297 | ✅ _Do_: Use themes to control the properties that you want to expose in your components.
298 |
299 | ```ts
300 | class ThemeProvider extends React.Component {
301 | render() {
302 | return (
303 |
304 | {this.props.children}
305 |
306 | );
307 | }
308 | }
309 |
310 | const theme = {
311 | colors: {
312 | primary: "purple",
313 | secondary: "blue",
314 | background: "light-grey",
315 | }
316 | };
317 | ```
318 |
319 | ❌ _Avoid_: Let component consumers override any style property from outside.
320 |
321 | ❔**Why?**
322 | Component producer need to control the functionality and the behavior of the component. Reducing the levels of freedom for the components consumers can provide a better predictability to the component's behavior, including its visual appearance.
323 |
324 | ### CSS Variables as theming variables
325 |
326 | ✅ _Do_: Use CSS variables for enabling theming. See [here](https://blog.logrocket.com/how-to-create-better-themes-with-css-variables-5a3744105c74/) for more details. CSS variables that can be used for theming should be documented as part of the component's APIs.
327 |
328 | ```css
329 | :root {
330 | --main-bg-color: fuchsia;
331 | }
332 | .button {
333 | background: fuchsia;
334 | background: var(--main-bg-color, fuchsia);
335 | }
336 | ```
337 |
338 | ❌ _Avoid_: Other theming techniques are also valid.
339 |
340 | ❔**Why?**
341 | CSS variables are framework independent and are supported by the browser. Also, CSS variables provide great flexibility as they can be scoped to different components.
342 |
343 | ## State Management
344 |
345 | Components may use state managers such as Redux, MobX, React Context, or VueX. State managers tend to be contextual and global. When reusing components between applications the consuming applications must have the same global context as the original one.
346 |
347 | ### Decouple data and layout
348 |
349 | ✅ _Do_: Separate presentational and container components. In most cases the data is specific to the consuming application. Component producers should provide presentational component only with APIs to get the data from a wrapping component that is managing data and state.
350 |
351 | ```ts
352 | //container component
353 | import React, { useState } from "react";
354 | import { Users } from '@src/presentation/users'
355 | export const UsersContainer = () => {
356 | const [users] = useState([
357 | { id: "8NaU7k", name: "John" },
358 | { id: "Wxxfs1", name: "Jane" }
359 | ]);
360 |
361 | return (
362 |
363 | );
364 | };
365 |
366 | //presentational component
367 | export const Users = (props) => {
368 | return (
369 |
370 |
371 | {props.data.map(user => (
372 | -
373 |
{user.name}
374 |
375 | ))}
376 |
377 |
378 | );
379 | };
380 | ```
381 |
382 | ❌ _Avoid_: sharing components that rely on a specific structure of data and enforce the consuming application to provide the data in a very specific format.
383 |
384 | ```ts
385 | //single component that manages both data and presentation
386 | import React, { useState } from "react";
387 |
388 | export const Users = () => {
389 | const [users] = useState([
390 | { id: "8NaU7k", name: "John" },
391 | { id: "Wxxfs1", name: "Jane" }
392 | ]);
393 |
394 | return (
395 |
396 |
397 | {users.map(user => (
398 | -
399 |
{user.name}
400 |
401 | ))}
402 |
403 |
404 | );
405 | };
406 | ```
407 |
408 | ## Distribution
409 |
410 | ### Package for distribution
411 |
412 | ✅ _Do_: Package your components in multiple formats that are supported by the various platforms:
413 | CommonJS (or cjs, or sometimes called es5 modules) for nodejs consumption
414 | ES Modules (sometimes also referenced as es6 modules), to be consumed by bundlers and enable tree shaking.
415 | UMD, for using directly in script tags in the browsers
416 | Use the package.json fields to direct to the different formats as per this convention:
417 |
418 | ```json
419 | # Package.json
420 | "main": "component.cjs.js"
421 | "module": "component.es.js"
422 | "browser": "component.umd.js"
423 | ```
424 |
425 | ❌ _Avoid_: Assuming the component is consumed in only a specific way:
426 |
427 | ❔**Why?**
428 | The JS world has a unique trait of a single interpreted language that is running on multiple platforms. Nodejs in various versions and various browsers create a plethora of runtime environments.
429 | Despite a continuous shift towards standard module handling, there are still a variety of methods to handle bundled code.
430 | During this struggling period and until we get one standard to rule them all, we should be good citizens of the JS-universe by feeding each packaging beast with its favorite flavor.
431 |
432 | For example, Webpack supports the different fields, according to the target it is building for.
433 |
434 | ```js
435 | //webpack.config.js
436 |
437 | //When the target property is set to webworker, web, or left unspecified:
438 | module.exports = {
439 | //...
440 | resolve: {
441 | mainFields: ['browser', 'module', 'main']
442 | }
443 | };
444 |
445 | //For any other target (including node):
446 |
447 | module.exports = {
448 | //...
449 | resolve: {
450 | mainFields: ['module', 'main']
451 | }
452 | };
453 | ```
454 | [Read more here](https://v4.webpack.js.org/configuration/resolve/#resolvemainfields)
455 |
--------------------------------------------------------------------------------
/images/trek_tweet.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tallyb/reusable-components-styleguide/305980cfb4136f143307bf7cdabe2579c09a24f7/images/trek_tweet.png
--------------------------------------------------------------------------------