Skip to content
This repository was archived by the owner on Nov 12, 2023. It is now read-only.

Commit 16d7c24

Browse files
committed
31. Introduction to reusable styles
1 parent 4a8ec13 commit 16d7c24

File tree

3 files changed

+331
-5
lines changed

3 files changed

+331
-5
lines changed

.vscode/settings.json

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"workbench.colorCustomizations": {
3+
"activityBar.background": "#3D156F",
4+
"titleBar.activeBackground": "#561D9B",
5+
"titleBar.activeForeground": "#FDFBFE"
6+
}
7+
}

README.md

+46-5
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
- [Exposing state via a callback](#exposing-state-via-a-callback)
3131
- [Invoking the useEffect callback only after mount!](#invoking-the-useeffect-callback-only-after-mount)
3232
- [**Section 5: Patterns for Crafting Reusable Styles**](#section-5-patterns-for-crafting-reusable-styles)
33+
- [Introduction to reusable styles](#introduction-to-reusable-styles)
3334
- [**Section 6: The Control Props Pattern**](#section-6-the-control-props-pattern)
3435
- [**Section 7: Custom Hooks: A Deeper Look at the Foundational Pattern**](#section-7-custom-hooks-a-deeper-look-at-the-foundational-pattern)
3536
- [**Section 8: The Props Collection Pattern**](#section-8-the-props-collection-pattern)
@@ -1031,18 +1032,24 @@ Pros
10311032
10321033
### How to implement the pattern
10331034
1035+
- Identify parent and all its children
1036+
- Create a context object in parent
1037+
- All children will hook up to context object
1038+
- All children can receive props from context object
1039+
1040+
Parent Component: MediumClap - use a Provider to pass the current value to the tree below
1041+
1042+
- Child Component: ClapIcon - get prop from context object
1043+
- Child Component: ClapCountTotal - get prop from context object
1044+
- Child Component: ClapCount - get prop from context object
1045+
10341046
1. Create a context object to pass values into the tree of child components in parent component
10351047
2. Use a Provider to pass the current value to the tree below
10361048
3. Returns a memoized state
10371049
4. Accepts a value prop to be passed to consuming components that are descendants of this Provider
10381050
5. Use the special children prop to pass children elements directly into Parent component
10391051
6. Get prop from context object instead from parent prop
10401052
1041-
Parent Component: MediumClap - use a Provider to pass the current value to the tree below
1042-
- Child Component: ClapIcon - get prop from context object
1043-
- Child Component: ClapCountTotal - get prop from context object
1044-
- Child Component: ClapCount - get prop from context object
1045-
10461053
```javascript
10471054
// 1. Create a context object to pass values into the tree of child components in parent component
10481055
const MediumClapContext = createContext()
@@ -1302,6 +1309,40 @@ const Usage = () => {
13021309
13031310
## **Section 5: Patterns for Crafting Reusable Styles**
13041311
1312+
### Introduction to reusable styles
1313+
1314+
- Regardless of the component you build, a common requirement is allowing the override and addition of new styles.
1315+
- Allow users style your components like any other element/component in their app.
1316+
1317+
JSX feature
1318+
1319+
- className: `<div className="red">Hello</div>`
1320+
- Inline style: `<div style={{color:'red'}}>Hello</div>`
1321+
1322+
Pass in className or style as props to reuse style
1323+
1324+
- Example: `<MediumClap className>Hello</MediumClap>`
1325+
- Example: `<MediumClap style>Hello</MediumClap>`
1326+
1327+
```javascript
1328+
// As with JSX elements styling via a className and style prop should be possible
1329+
<YourComponent className=`shouldWork`/>
1330+
1331+
<YourComponent style=`shouldWork`/>
1332+
```
1333+
1334+
Open-source examples
1335+
- [Reach UI](https://reacttraining.com/reach-ui/)
1336+
1337+
| Pros | Cons |
1338+
| :------------------------ | :--- |
1339+
| Intuitive Style Overrides | |
1340+
1341+
Pros
1342+
1343+
- Intuitive Style Overrides
1344+
- Allow for style overrides in a way your users are already familiar with..
1345+
13051346
**[⬆ back to top](#table-of-contents)**
13061347
13071348
## **Section 6: The Control Props Pattern**

showcase/src/patterns/04.js

+278
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
import React, {
2+
useState,
3+
useLayoutEffect,
4+
useCallback,
5+
createContext,
6+
useMemo,
7+
useContext,
8+
useEffect,
9+
useRef
10+
} from 'react'
11+
import mojs from 'mo-js'
12+
import styles from './index.css'
13+
14+
const initialState = {
15+
count: 0,
16+
countTotal: 267,
17+
isClicked: false
18+
}
19+
20+
// Custom Hook for animaton
21+
const useClapAnimation = ({
22+
clapEl,
23+
clapCountEl,
24+
clapCountTotalEl
25+
}) => {
26+
27+
// Do not write useState(new mojs.Timeline())
28+
// if not every single time useClapAnimation is called
29+
// new mojs.Timeline() is involved
30+
const [animationTimeline, setAnimationTimeline] = useState(() => new mojs.Timeline())
31+
32+
useLayoutEffect(() => {
33+
if(!clapEl || !clapCountEl || !clapCountTotalEl) return
34+
const tlDuration = 300
35+
const scaleButton = new mojs.Html({
36+
el: clapEl,
37+
duration: tlDuration,
38+
// scale from [t=0, 1.3] to [t=300, 1]
39+
scale: { 1.3 : 1 },
40+
easing: mojs.easing.ease.out
41+
})
42+
43+
const countTotalAnimation = new mojs.Html({
44+
el: clapCountTotalEl,
45+
delay: (3 * tlDuration) / 2,
46+
duration: tlDuration,
47+
// opacity from [t=delay, 0] to [t=300, 1]
48+
opacity: { 0 : 1 },
49+
// move up y axis from [t=delay, y=0] to [t=300, y=-3]
50+
y: { 0 : -3 }
51+
})
52+
53+
const countAnimation = new mojs.Html({
54+
el: clapCountEl,
55+
duration: tlDuration,
56+
opacity: { 0 : 1 },
57+
y: { 0 : -30 }
58+
}).then({
59+
// next animation to fade out
60+
delay: tlDuration / 2,
61+
opacity: { 1 : 0 },
62+
y: -80
63+
})
64+
65+
// scale back to 1 before animation start
66+
if(typeof clapEl === 'string') {
67+
const clap = document.getElementById('clap')
68+
clap.style.transform = 'scale(1,1)'
69+
} else {
70+
clapEl.style.transform = 'scale(1,1)'
71+
}
72+
73+
// particle effect burst
74+
const triangleBurst = new mojs.Burst({
75+
parent: clapEl,
76+
// radius from [t=0, 50] to [t=300, 95]
77+
radius: { 50 : 95 },
78+
count: 5,
79+
angle: 30,
80+
children: {
81+
// default is triangle
82+
shape: 'polygon',
83+
radius: { 6 : 0 },
84+
stroke: 'rgba(211,54,0,0.5)',
85+
strokeWidth: 2,
86+
// angle of each particle
87+
angle: 210,
88+
speed: 0.2,
89+
delay: 30,
90+
easing: mojs.easing.bezier(0.1, 1, 0.3, 1),
91+
duration: tlDuration,
92+
}
93+
})
94+
95+
const circleBurst = new mojs.Burst({
96+
parent: clapEl,
97+
radius: { 50: 75 },
98+
angle: 25,
99+
duration: tlDuration,
100+
children: {
101+
shape: 'circle',
102+
fill: 'rgba(149,165,166,0.5)',
103+
delay: 30,
104+
speed: 0.2,
105+
radius: { 3 : 0 },
106+
easing: mojs.easing.bezier(0.1, 1, 0.3, 1)
107+
}
108+
})
109+
110+
const newAnimationTimeline = animationTimeline.add(
111+
[
112+
scaleButton,
113+
countTotalAnimation,
114+
countAnimation,
115+
triangleBurst,
116+
circleBurst
117+
])
118+
setAnimationTimeline(newAnimationTimeline)
119+
}, [clapEl, clapCountEl, clapCountTotalEl])
120+
121+
return animationTimeline;
122+
}
123+
124+
// MediumClapContext lets us pass a value deep into the tree of React components in MediumClap component
125+
const MediumClapContext = createContext()
126+
127+
// Use a Provider to pass the current value to the tree below.
128+
// Any component can read it, no matter how deep it is.
129+
const { Provider } = MediumClapContext
130+
131+
// MediumClap component don’t know their children ahead of time.
132+
// Use the special children prop to pass children elements directly into MediumClap
133+
const MediumClap = ({ children, onClap }) => {
134+
const MAXIMUM_USER_CLAP = 50
135+
const [clapState, setClapState] = useState(initialState)
136+
const { count, countTotal, isClicked } = clapState
137+
138+
const [{ clapRef, clapCountRef, clapCountTotalRef }, setRefState] = useState({})
139+
const setRef = useCallback(node => {
140+
setRefState(prevRefState => ({
141+
...prevRefState,
142+
[node.dataset.refkey]: node
143+
}))
144+
}, [])
145+
146+
const animationTimeline = useClapAnimation({
147+
clapEl: clapRef,
148+
clapCountEl: clapCountRef,
149+
clapCountTotalEl: clapCountTotalRef
150+
})
151+
152+
// 1. default value is false
153+
// 1. set to true when MediumClap is rendered
154+
const componentJustMounted = useRef(false)
155+
useEffect(() => {
156+
if(componentJustMounted.current){
157+
// 3. next time count changes
158+
console.log('onClap is called')
159+
onClap && onClap(clapState)
160+
} else {
161+
// 2. set to true the first time in useEffect after rendered
162+
componentJustMounted.current = true
163+
}
164+
}, [count])
165+
166+
const handleClapClick = () => {
167+
animationTimeline.replay()
168+
setClapState(prevState => ({
169+
count: Math.min(prevState.count + 1, MAXIMUM_USER_CLAP),
170+
countTotal:
171+
count < MAXIMUM_USER_CLAP
172+
? prevState.countTotal + 1
173+
: prevState.countTotal,
174+
isClicked: true,
175+
}))
176+
}
177+
178+
// Returns a memoized state.
179+
const memoizedValue = useMemo(
180+
() => ({
181+
...clapState,
182+
setRef
183+
}), [clapState, setRef]
184+
)
185+
186+
return (
187+
// Accepts a value prop to be passed to consuming components that are descendants of this Provider.
188+
// MediumClap component don’t know their children ahead of time.
189+
// Use the special children prop to pass children elements directly into MediumClap
190+
<Provider value={memoizedValue}>
191+
<button
192+
ref={setRef}
193+
data-refkey="clapRef"
194+
className={styles.clap}
195+
onClick={handleClapClick}
196+
>
197+
{children}
198+
</button>
199+
</Provider>
200+
)
201+
}
202+
203+
const ClapIcon = () => {
204+
// Get prop from MediumClapContext instead from parent MediumClap
205+
const { isClicked } = useContext(MediumClapContext)
206+
return (
207+
<span>
208+
<svg
209+
xmlns='http://www.w3.org/2000/svg'
210+
viewBox='-549 338 100.1 125'
211+
className={`${styles.icon} ${isClicked && styles.checked}`}
212+
>
213+
<path d='M-471.2 366.8c1.2 1.1 1.9 2.6 2.3 4.1.4-.3.8-.5 1.2-.7 1-1.9.7-4.3-1-5.9-2-1.9-5.2-1.9-7.2.1l-.2.2c1.8.1 3.6.9 4.9 2.2zm-28.8 14c.4.9.7 1.9.8 3.1l16.5-16.9c.6-.6 1.4-1.1 2.1-1.5 1-1.9.7-4.4-.9-6-2-1.9-5.2-1.9-7.2.1l-15.5 15.9c2.3 2.2 3.1 3 4.2 5.3zm-38.9 39.7c-.1-8.9 3.2-17.2 9.4-23.6l18.6-19c.7-2 .5-4.1-.1-5.3-.8-1.8-1.3-2.3-3.6-4.5l-20.9 21.4c-10.6 10.8-11.2 27.6-2.3 39.3-.6-2.6-1-5.4-1.1-8.3z' />
214+
<path d='M-527.2 399.1l20.9-21.4c2.2 2.2 2.7 2.6 3.5 4.5.8 1.8 1 5.4-1.6 8l-11.8 12.2c-.5.5-.4 1.2 0 1.7.5.5 1.2.5 1.7 0l34-35c1.9-2 5.2-2.1 7.2-.1 2 1.9 2 5.2.1 7.2l-24.7 25.3c-.5.5-.4 1.2 0 1.7.5.5 1.2.5 1.7 0l28.5-29.3c2-2 5.2-2 7.1-.1 2 1.9 2 5.1.1 7.1l-28.5 29.3c-.5.5-.4 1.2 0 1.7.5.5 1.2.4 1.7 0l24.7-25.3c1.9-2 5.1-2.1 7.1-.1 2 1.9 2 5.2.1 7.2l-24.7 25.3c-.5.5-.4 1.2 0 1.7.5.5 1.2.5 1.7 0l14.6-15c2-2 5.2-2 7.2-.1 2 2 2.1 5.2.1 7.2l-27.6 28.4c-11.6 11.9-30.6 12.2-42.5.6-12-11.7-12.2-30.8-.6-42.7m18.1-48.4l-.7 4.9-2.2-4.4m7.6.9l-3.7 3.4 1.2-4.8m5.5 4.7l-4.8 1.6 3.1-3.9' />
215+
</svg>
216+
</span>
217+
)
218+
}
219+
220+
const ClapCount = () => {
221+
// Get prop from MediumClapContext instead from parent MediumClap
222+
const { count, setRef } = useContext(MediumClapContext)
223+
return (
224+
<span
225+
ref={setRef}
226+
data-refkey="clapCountRef"
227+
className={styles.count}
228+
>
229+
+ {count}
230+
</span>
231+
)
232+
}
233+
234+
const ClapCountTotal = () => {
235+
// Get prop from MediumClapContext instead from parent MediumClap
236+
const { countTotal, setRef } = useContext(MediumClapContext)
237+
return (
238+
<span
239+
ref={setRef}
240+
data-refkey="clapCountTotalRef"
241+
className={styles.total}
242+
>
243+
{countTotal}
244+
</span>
245+
)
246+
}
247+
248+
MediumClap.Icon = ClapIcon
249+
MediumClap.Count = ClapCount
250+
MediumClap.Total = ClapCountTotal
251+
// import MediumClap, { Icon, Count, Total } from 'medium-clap'
252+
253+
// MediumClap component don’t know their children ahead of time.
254+
// Use the special children prop to pass children elements directly into MediumClap
255+
const Usage = () => {
256+
// expose count, countTotal and isClicked
257+
const [count, setCount] = useState(0)
258+
const handleClap = (clapState) => {
259+
setCount(clapState.count)
260+
}
261+
// count = 0
262+
// !count = true
263+
// !!count = false
264+
return (
265+
<div style={{ width: '100%' }}>
266+
<MediumClap onClap={handleClap}>
267+
<MediumClap.Icon />
268+
<MediumClap.Count />
269+
<MediumClap.Total />
270+
</MediumClap>
271+
{!!count && (
272+
<div className={styles.info}>{`You have clapped ${count} times`}</div>
273+
)}
274+
</div>
275+
)
276+
}
277+
278+
export default Usage

0 commit comments

Comments
 (0)