Repo: https://github.com/ohansemmanuel/advanced-react-patterns-ultrasimplified
El código inicial, con un HOC, es:
import React, { Component, useState } from 'react'
import mojs from 'mo-js'
import styles from './index.css'
const initialState = {
count: 0,
countTotal: 267,
isClicked: false
}
/**
* Higher Order Component
*/
const withClapAnimation = WrappedComponent => {
class WithClapAnimation extends Component {
animationTimeline = new mojs.Timeline()
state = {
animationTimeline: this.animationTimeline
}
componentDidMount () {
const tlDuration = 300
const scaleButton = new mojs.Html({
el: '#clap',
duration: tlDuration,
scale: { 1.3: 1 },
easing: mojs.easing.ease.out
})
const countAnimation = new mojs.Html({
el: '#clapCount',
opacity: { 0: 1 },
y: { 0: -30 },
duration: tlDuration
}).then({
opacity: { 1: 0 },
y: -80,
delay: tlDuration / 2
})
const countTotalAnimation = new mojs.Html({
el: '#clapCountTotal',
opacity: { 0: 1 },
delay: (3 * tlDuration) / 2,
duration: tlDuration,
y: { 0: -3 }
})
const clap = document.getElementById('clap')
clap.style.transform = 'scale(1,1)'
const newAnimationTimeline = this.animationTimeline.add([
scaleButton,
countTotalAnimation,
countAnimation
])
this.setState({ animationTimeline: newAnimationTimeline })
}
render () {
return (
<WrappedComponent
{...this.props}
animationTimeline={this.state.animationTimeline}
/>
)
}
}
return WithClapAnimation
}
const MediumClap = ({ animationTimeline }) => {
const MAXIMUM_USER_CLAP = 50
const [clapState, setClapState] = useState(initialState)
const { count, countTotal, isClicked } = clapState
const handleClapClick = () => {
animationTimeline.replay()
setClapState(prevState => ({
isClicked: true,
count: Math.min(count + 1, MAXIMUM_USER_CLAP),
countTotal:
count < MAXIMUM_USER_CLAP
? prevState.countTotal + 1
: prevState.countTotal
}))
}
return (
<button id='clap' className={styles.clap} onClick={handleClapClick}>
<ClapIcon isClicked={isClicked} />
<ClapCount count={count} />
<CountTotal countTotal={countTotal} />
</button>
)
}
/**
* subcomponents
*/
const ClapIcon = ({ isClicked }) => {
return (
<span>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='-549 338 100.1 125'
className={`${styles.icon} ${isClicked && styles.checked}`}
>
<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' />
<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' />
</svg>
</span>
)
}
const ClapCount = ({ count }) => {
return (
<span id='clapCount' className={styles.count}>
+ {count}
</span>
)
}
const CountTotal = ({ countTotal }) => {
return (
<span id='clapCountTotal' className={styles.total}>
{countTotal}
</span>
)
}
/**
* Usage
*/
const Usage = () => {
const AnimatedMediumClap = withClapAnimation(MediumClap)
return <AnimatedMediumClap />
}
export default Usage
Y pasándolo a componentes funcionales, con un custom hook:
import React, { Component, useState, useEffect } from 'react'
import mojs from 'mo-js'
import styles from './index.css'
const initialState = {
count: 0,
countTotal: 267,
isClicked: false
}
/**
* Custom Hook for animation
*/
const useClapAnimation = () => {
const [animationTimeline, setAnimationTimeline] = useState(
() => new mojs.Timeline()
)
useEffect(() => {
const tlDuration = 300
const scaleButton = new mojs.Html({
el: '#clap',
duration: tlDuration,
scale: { 1.3: 1 },
easing: mojs.easing.ease.out
})
const triangleBurst = new mojs.Burst({
parent: '#clap',
radius: { 50: 95 },
count: 5,
angle: 30,
children: {
shape: 'polygon',
radius: { 6: 0 },
stroke: 'rgba(211,54,0,0.5)',
strokeWidth: 2,
angle: 210,
delay: 30,
speed: 0.2,
easing: mojs.easing.bezier(0.1, 1, 0.3, 1),
duration: tlDuration
}
})
const circleBurst = new mojs.Burst({
parent: '#clap',
radius: { 50: 75 },
angle: 25,
duration: tlDuration,
children: {
shape: 'circle',
fill: 'rgba(149,165,166,0.5)',
delay: 30,
speed: 0.2,
radius: { 3: 0 },
easing: mojs.easing.bezier(0.1, 1, 0.3, 1)
}
})
const countAnimation = new mojs.Html({
el: '#clapCount',
opacity: { 0: 1 },
y: { 0: -30 },
duration: tlDuration
}).then({
opacity: { 1: 0 },
y: -80,
delay: tlDuration / 2
})
const countTotalAnimation = new mojs.Html({
el: '#clapCountTotal',
opacity: { 0: 1 },
delay: (3 * tlDuration) / 2,
duration: tlDuration,
y: { 0: -3 }
})
const clap = document.getElementById('clap')
clap.style.transform = 'scale(1,1)'
const newAnimationTimeline = animationTimeline.add([
scaleButton,
countTotalAnimation,
countAnimation,
triangleBurst,
circleBurst
])
setAnimationTimeline(newAnimationTimeline)
}, [])
return animationTimeline
}
const MediumClap = () => {
const MAXIMUM_USER_CLAP = 50
const [clapState, setClapState] = useState(initialState)
const { count, countTotal, isClicked } = clapState
const animationTimeline = useClapAnimation()
const handleClapClick = () => {
animationTimeline.replay()
setClapState(prevState => ({
isClicked: true,
count: Math.min(count + 1, MAXIMUM_USER_CLAP),
countTotal:
count < MAXIMUM_USER_CLAP
? prevState.countTotal + 1
: prevState.countTotal
}))
}
return (
<button id='clap' className={styles.clap} onClick={handleClapClick}>
<ClapIcon isClicked={isClicked} />
<ClapCount count={count} />
<CountTotal countTotal={countTotal} />
</button>
)
}
/**
* subcomponents
*/
const ClapIcon = ({ isClicked }) => {
return (
<span>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='-549 338 100.1 125'
className={`${styles.icon} ${isClicked && styles.checked}`}
>
<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' />
<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' />
</svg>
</span>
)
}
const ClapCount = ({ count }) => {
return (
<span id='clapCount' className={styles.count}>
+ {count}
</span>
)
}
const CountTotal = ({ countTotal }) => {
return (
<span id='clapCountTotal' className={styles.total}>
{countTotal}
</span>
)
}
/**
* Usage
*/
const Usage = () => {
return <MediumClap />
}
export default Usage
El problema es que las referencias en las animaciones se hacen por #id
. Entonces, si tenemos más de un botón, las animaciones se aplican sobre todos los botones, y solo se debe aplicar sobre el botón que se pulsa.
Para resolver esto, se hace uso de refs
y de useCallback
. Hace uso del ref como callback (https://es.reactjs.org/docs/refs-and-the-dom.html#callback-refs), y useCallback
(https://reactjs.org/docs/hooks-reference.html#usecallback) para memoizar el callback.
Y para rellenar el estado en el callback, y relacionar el nodo que se recibe con el nombre del objeto en el estado, se "inventa" un atributo data-refKey
, donde guarda el nombre (https://www.w3schools.com/tags/att_global_data.asp).
import React, { useState, useEffect, useCallback } from 'react'
import mojs from 'mo-js'
import styles from './index.css'
const initialState = {
count: 0,
countTotal: 267,
isClicked: false
}
/**
* Custom Hook for animation
*/
const useClapAnimation = ({ clapEl, countEl, clapTotalEl }) => {
const [animationTimeline, setAnimationTimeline] = useState(
() => new mojs.Timeline()
)
useEffect(() => {
const tlDuration = 300
const scaleButton = new mojs.Html({
el: clapEl,
duration: tlDuration,
scale: { 1.3: 1 },
easing: mojs.easing.ease.out
})
const triangleBurst = new mojs.Burst({
parent: clapEl,
radius: { 50: 95 },
count: 5,
angle: 30,
children: {
shape: 'polygon',
radius: { 6: 0 },
stroke: 'rgba(211,54,0,0.5)',
strokeWidth: 2,
angle: 210,
delay: 30,
speed: 0.2,
easing: mojs.easing.bezier(0.1, 1, 0.3, 1),
duration: tlDuration
}
})
const circleBurst = new mojs.Burst({
parent: clapEl,
radius: { 50: 75 },
angle: 25,
duration: tlDuration,
children: {
shape: 'circle',
fill: 'rgba(149,165,166,0.5)',
delay: 30,
speed: 0.2,
radius: { 3: 0 },
easing: mojs.easing.bezier(0.1, 1, 0.3, 1)
}
})
const countAnimation = new mojs.Html({
el: countEl,
opacity: { 0: 1 },
y: { 0: -30 },
duration: tlDuration
}).then({
opacity: { 1: 0 },
y: -80,
delay: tlDuration / 2
})
const countTotalAnimation = new mojs.Html({
el: clapTotalEl,
opacity: { 0: 1 },
delay: (3 * tlDuration) / 2,
duration: tlDuration,
y: { 0: -3 }
})
if (typeof clapEl === 'string') {
const clap = document.getElementById('clap')
clap.style.transform = 'scale(1,1)'
} else {
clapEl.style.transform = 'scale(1,1)'
}
const newAnimationTimeline = animationTimeline.add([
scaleButton,
countTotalAnimation,
countAnimation,
triangleBurst,
circleBurst
])
setAnimationTimeline(newAnimationTimeline)
}, [])
return animationTimeline
}
const MediumClap = () => {
const MAXIMUM_USER_CLAP = 50
const [clapState, setClapState] = useState(initialState)
const { count, countTotal, isClicked } = clapState
const [{ clapRef, clapCountRef, clapTotalRef }, setRefState] = useState({})
const setRef = useCallback(node => {
setRefState(prevRefState => ({
...prevRefState,
[node.dataset.refkey]: node
}))
}, [])
const animationTimeline = useClapAnimation({
clapEl: clapRef,
countEl: clapCountRef,
clapTotalEl: clapTotalRef
})
const handleClapClick = () => {
animationTimeline.replay()
setClapState(prevState => ({
isClicked: true,
count: Math.min(count + 1, MAXIMUM_USER_CLAP),
countTotal:
count < MAXIMUM_USER_CLAP
? prevState.countTotal + 1
: prevState.countTotal
}))
}
return (
<button
ref={setRef}
data-refkey='clapRef'
className={styles.clap}
onClick={handleClapClick}
>
<ClapIcon isClicked={isClicked} />
<ClapCount count={count} setRef={setRef} />
<CountTotal countTotal={countTotal} setRef={setRef} />
</button>
)
}
/**
* subcomponents
*/
const ClapIcon = ({ isClicked }) => {
return (
<span>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='-549 338 100.1 125'
className={`${styles.icon} ${isClicked && styles.checked}`}
>
<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' />
<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' />
</svg>
</span>
)
}
const ClapCount = ({ count, setRef }) => {
return (
<span ref={setRef} data-refkey='clapCountRef' className={styles.count}>
+ {count}
</span>
)
}
const CountTotal = ({ countTotal, setRef }) => {
return (
<span ref={setRef} data-refkey='clapTotalRef' className={styles.total}>
{countTotal}
</span>
)
}
/**
* Usage
*/
const Usage = () => {
return <MediumClap />
}
export default Usage
Tenemos el componente Usage
, que llama a MediumClap
.
MediumClap
inicializa su estado, y entre ellos, inicializa { clapRef, clapCountRef, clapTotalRef },
como {}
.
En el custom hook useClapAnimation
se le pasa este objeto vacío, y en ese hook se debería iniciar el useEffect
, pero realmente no se llama! porque aún no ha hecho el render el componente MediumClap
.
Una vez hace el render, ahora sí se llama a useEffect
, pero tiene las referencias a las props vacías que recibe, y como aplica estos objetos vacíos como los elementos sobre los que mo.js tiene que realizar las animaciones, da un error.
Para corregirlo, añade en el useEffect
un control de que los elementos no sean vacíos.
Ahora el problema es que useEffect
se llama solo esta primera vez, por lo tanto sigue sin funcionar.
Cuando se hace el render, se llama a setRef
, que provoca que se actualice el estado de MediumClap
, y para que esto provoque que se llame de nuevo al useEffect
, hay que añadirle un nuevo parámetro final con el array de objetos que provocan que useEffect
vuelva a lanzarse si alguno de ellos cambia: [clapEl, countEl, clapTotalEl]
.
El resultado es:
import React, { useState, useLayoutEffect, useCallback } from 'react'
import mojs from 'mo-js'
import styles from './index.css'
const initialState = {
count: 0,
countTotal: 267,
isClicked: false
}
/**
* Custom Hook for animation
*/
const useClapAnimation = ({ clapEl, countEl, clapTotalEl }) => {
const [animationTimeline, setAnimationTimeline] = useState(
() => new mojs.Timeline()
)
useLayoutEffect(() => {
if (!clapEl || !countEl || !clapTotalEl) {
return
}
const tlDuration = 300
const scaleButton = new mojs.Html({
el: clapEl,
duration: tlDuration,
scale: { 1.3: 1 },
easing: mojs.easing.ease.out
})
const triangleBurst = new mojs.Burst({
parent: clapEl,
radius: { 50: 95 },
count: 5,
angle: 30,
children: {
shape: 'polygon',
radius: { 6: 0 },
stroke: 'rgba(211,54,0,0.5)',
strokeWidth: 2,
angle: 210,
delay: 30,
speed: 0.2,
easing: mojs.easing.bezier(0.1, 1, 0.3, 1),
duration: tlDuration
}
})
const circleBurst = new mojs.Burst({
parent: clapEl,
radius: { 50: 75 },
angle: 25,
duration: tlDuration,
children: {
shape: 'circle',
fill: 'rgba(149,165,166,0.5)',
delay: 30,
speed: 0.2,
radius: { 3: 0 },
easing: mojs.easing.bezier(0.1, 1, 0.3, 1)
}
})
const countAnimation = new mojs.Html({
el: countEl,
opacity: { 0: 1 },
y: { 0: -30 },
duration: tlDuration
}).then({
opacity: { 1: 0 },
y: -80,
delay: tlDuration / 2
})
const countTotalAnimation = new mojs.Html({
el: clapTotalEl,
opacity: { 0: 1 },
delay: (3 * tlDuration) / 2,
duration: tlDuration,
y: { 0: -3 }
})
if (typeof clapEl === 'string') {
const clap = document.getElementById('clap')
clap.style.transform = 'scale(1,1)'
} else {
clapEl.style.transform = 'scale(1,1)'
}
const newAnimationTimeline = animationTimeline.add([
scaleButton,
countTotalAnimation,
countAnimation,
triangleBurst,
circleBurst
])
setAnimationTimeline(newAnimationTimeline)
}, [clapEl, countEl, clapTotalEl])
return animationTimeline
}
const MediumClap = () => {
const MAXIMUM_USER_CLAP = 50
const [clapState, setClapState] = useState(initialState)
const { count, countTotal, isClicked } = clapState
const [{ clapRef, clapCountRef, clapTotalRef }, setRefState] = useState({})
const setRef = useCallback(node => {
setRefState(prevRefState => ({
...prevRefState,
[node.dataset.refkey]: node
}))
}, [])
const animationTimeline = useClapAnimation({
clapEl: clapRef,
countEl: clapCountRef,
clapTotalEl: clapTotalRef
})
const handleClapClick = () => {
animationTimeline.replay()
setClapState(prevState => ({
isClicked: true,
count: Math.min(count + 1, MAXIMUM_USER_CLAP),
countTotal:
count < MAXIMUM_USER_CLAP
? prevState.countTotal + 1
: prevState.countTotal
}))
}
return (
<button
ref={setRef}
data-refkey='clapRef'
className={styles.clap}
onClick={handleClapClick}
>
<ClapIcon isClicked={isClicked} />
<ClapCount count={count} setRef={setRef} />
<CountTotal countTotal={countTotal} setRef={setRef} />
</button>
)
}
/**
* subcomponents
*/
const ClapIcon = ({ isClicked }) => {
return (
<span>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='-549 338 100.1 125'
className={`${styles.icon} ${isClicked && styles.checked}`}
>
<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' />
<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' />
</svg>
</span>
)
}
const ClapCount = ({ count, setRef }) => {
return (
<span ref={setRef} data-refkey='clapCountRef' className={styles.count}>
+ {count}
</span>
)
}
const CountTotal = ({ countTotal, setRef }) => {
return (
<span ref={setRef} data-refkey='clapTotalRef' className={styles.total}>
{countTotal}
</span>
)
}
/**
* Usage
*/
const Usage = () => {
return <MediumClap />
}
export default Usage
Consiste en hacer una composición como esta:
<Padre>
<Hijo1/>
<Hijo2/>
</Padre>
Una opción es que el componente padre pase a sus hijos las propiedades por el contexto.
Así, tendríamos lo siguiente:
import React, {
useState,
useLayoutEffect,
useCallback,
createContext,
useMemo,
useContext,
useEffect,
useRef
} from 'react'
import mojs from 'mo-js'
import styles from './index.css'
const initialState = {
count: 0,
countTotal: 267,
isClicked: false
}
/**
* Custom Hook for animation
*/
const useClapAnimation = ({ clapEl, countEl, clapTotalEl }) => {
const [animationTimeline, setAnimationTimeline] = useState(
() => new mojs.Timeline()
)
useLayoutEffect(() => {
if (!clapEl || !countEl || !clapTotalEl) {
return
}
const tlDuration = 300
const scaleButton = new mojs.Html({
el: clapEl,
duration: tlDuration,
scale: { 1.3: 1 },
easing: mojs.easing.ease.out
})
const triangleBurst = new mojs.Burst({
parent: clapEl,
radius: { 50: 95 },
count: 5,
angle: 30,
children: {
shape: 'polygon',
radius: { 6: 0 },
stroke: 'rgba(211,54,0,0.5)',
strokeWidth: 2,
angle: 210,
delay: 30,
speed: 0.2,
easing: mojs.easing.bezier(0.1, 1, 0.3, 1),
duration: tlDuration
}
})
const circleBurst = new mojs.Burst({
parent: clapEl,
radius: { 50: 75 },
angle: 25,
duration: tlDuration,
children: {
shape: 'circle',
fill: 'rgba(149,165,166,0.5)',
delay: 30,
speed: 0.2,
radius: { 3: 0 },
easing: mojs.easing.bezier(0.1, 1, 0.3, 1)
}
})
const countAnimation = new mojs.Html({
el: countEl,
opacity: { 0: 1 },
y: { 0: -30 },
duration: tlDuration
}).then({
opacity: { 1: 0 },
y: -80,
delay: tlDuration / 2
})
const countTotalAnimation = new mojs.Html({
el: clapTotalEl,
opacity: { 0: 1 },
delay: (3 * tlDuration) / 2,
duration: tlDuration,
y: { 0: -3 }
})
if (typeof clapEl === 'string') {
const clap = document.getElementById('clap')
clap.style.transform = 'scale(1,1)'
} else {
clapEl.style.transform = 'scale(1,1)'
}
const newAnimationTimeline = animationTimeline.add([
scaleButton,
countTotalAnimation,
countAnimation,
triangleBurst,
circleBurst
])
setAnimationTimeline(newAnimationTimeline)
}, [clapEl, countEl, clapTotalEl])
return animationTimeline
}
const MediumClapContext = createContext()
const { Provider } = MediumClapContext
const MediumClap = ({ children, onClap }) => {
const MAXIMUM_USER_CLAP = 50
const [clapState, setClapState] = useState(initialState)
const { count } = clapState
const [{ clapRef, clapCountRef, clapTotalRef }, setRefState] = useState({})
const setRef = useCallback(node => {
setRefState(prevRefState => ({
...prevRefState,
[node.dataset.refkey]: node
}))
}, [])
const animationTimeline = useClapAnimation({
clapEl: clapRef,
countEl: clapCountRef,
clapTotalEl: clapTotalRef
})
const componentJustMounted = useRef(true)
useEffect(() => {
if (!componentJustMounted.current) {
console.log('onClap was called!!!')
onClap && onClap(clapState)
}
componentJustMounted.current = false
}, [count])
const handleClapClick = () => {
animationTimeline.replay()
setClapState(prevState => ({
isClicked: true,
count: Math.min(count + 1, MAXIMUM_USER_CLAP),
countTotal:
count < MAXIMUM_USER_CLAP
? prevState.countTotal + 1
: prevState.countTotal
}))
}
const memoizedValue = useMemo(
() => ({
...clapState,
setRef
}),
[clapState, setRef]
)
return (
<Provider value={memoizedValue}>
<button
ref={setRef}
data-refkey='clapRef'
className={styles.clap}
onClick={handleClapClick}
>
{children}
</button>
</Provider>
)
}
/**
* subcomponents
*/
const ClapIcon = () => {
const { isClicked } = useContext(MediumClapContext)
return (
<span>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='-549 338 100.1 125'
className={`${styles.icon} ${isClicked && styles.checked}`}
>
<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' />
<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' />
</svg>
</span>
)
}
const ClapCount = () => {
const { count, setRef } = useContext(MediumClapContext)
return (
<span ref={setRef} data-refkey='clapCountRef' className={styles.count}>
+ {count}
</span>
)
}
const CountTotal = () => {
const { countTotal, setRef } = useContext(MediumClapContext)
return (
<span ref={setRef} data-refkey='clapTotalRef' className={styles.total}>
{countTotal}
</span>
)
}
MediumClap.Icon = ClapIcon
MediumClap.Count = ClapCount
MediumClap.Total = CountTotal
/**
* Usage
*/
// import MediumClap from 'medium-clap'
const Usage = () => {
const [count, setCount] = useState(0)
const handleClap = clapState => {
setCount(clapState.count)
}
return (
<div style={{ width: '100%' }}>
<MediumClap onClap={handleClap}>
<MediumClap.Icon />
<MediumClap.Count />
<MediumClap.Total />
</MediumClap>
{!!count && (
<div className={styles.info}>{`You have clapped ${count} times`}</div>
)}
</div>
)
}
export default Usage
Para no tener que importarte el componente padre y todos sus hijos, sino que solo haga falta importar el padre, se puede hacer lo siguiente, como se ve en el punto anterior:
MediumClap.Icon = ClapIcon
MediumClap.Count = ClapCount
MediumClap.Total = CountTotal
Una forma de que el usuario pueda cambiar los estilos de un componente, es que pueda pasar una prop style
al componente, y que este aplique esos estilos al componente.
Así, el componente será:
const MyComponent = ({style: userStyles = {}}) => {
...
return (
<div style={userStyles}>
...
</div>
)
}
Y luego, el componente se puede usar y reescribir su estilo:
<MyComponent style={{border: 'red'}}>
Otra forma de poder cambiar estilos es mediante el className
.
Si tengo un custom.css
:
.clap {....}
Y lo quiero usar en un componente.
En este componente tendrá que crearse un className
que mergee lo que ya tenía antes con lo que se le pase por props.
En el ejemplo está hecho así:
import React, {
useState,
useLayoutEffect,
useCallback,
createContext,
useMemo,
useContext,
useEffect,
useRef
} from 'react'
import mojs from 'mo-js'
import styles from './index.css'
import userCustomStyles from './usage.css'
const initialState = {
count: 0,
countTotal: 267,
isClicked: false
}
/**
* Custom Hook for animation
*/
const useClapAnimation = ({ clapEl, countEl, clapTotalEl }) => {
const [animationTimeline, setAnimationTimeline] = useState(
() => new mojs.Timeline()
)
useLayoutEffect(() => {
if (!clapEl || !countEl || !clapTotalEl) {
return
}
const tlDuration = 300
const scaleButton = new mojs.Html({
el: clapEl,
duration: tlDuration,
scale: { 1.3: 1 },
easing: mojs.easing.ease.out
})
const triangleBurst = new mojs.Burst({
parent: clapEl,
radius: { 50: 95 },
count: 5,
angle: 30,
children: {
shape: 'polygon',
radius: { 6: 0 },
stroke: 'rgba(211,54,0,0.5)',
strokeWidth: 2,
angle: 210,
delay: 30,
speed: 0.2,
easing: mojs.easing.bezier(0.1, 1, 0.3, 1),
duration: tlDuration
}
})
const circleBurst = new mojs.Burst({
parent: clapEl,
radius: { 50: 75 },
angle: 25,
duration: tlDuration,
children: {
shape: 'circle',
fill: 'rgba(149,165,166,0.5)',
delay: 30,
speed: 0.2,
radius: { 3: 0 },
easing: mojs.easing.bezier(0.1, 1, 0.3, 1)
}
})
const countAnimation = new mojs.Html({
el: countEl,
opacity: { 0: 1 },
y: { 0: -30 },
duration: tlDuration
}).then({
opacity: { 1: 0 },
y: -80,
delay: tlDuration / 2
})
const countTotalAnimation = new mojs.Html({
el: clapTotalEl,
opacity: { 0: 1 },
delay: (3 * tlDuration) / 2,
duration: tlDuration,
y: { 0: -3 }
})
if (typeof clapEl === 'string') {
const clap = document.getElementById('clap')
clap.style.transform = 'scale(1,1)'
} else {
clapEl.style.transform = 'scale(1,1)'
}
const newAnimationTimeline = animationTimeline.add([
scaleButton,
countTotalAnimation,
countAnimation,
triangleBurst,
circleBurst
])
setAnimationTimeline(newAnimationTimeline)
}, [clapEl, countEl, clapTotalEl])
return animationTimeline
}
const MediumClapContext = createContext()
const { Provider } = MediumClapContext
const MediumClap = ({
children,
onClap,
style: userStyles = {},
className
}) => {
const MAXIMUM_USER_CLAP = 50
const [clapState, setClapState] = useState(initialState)
const { count } = clapState
const [{ clapRef, clapCountRef, clapTotalRef }, setRefState] = useState({})
const setRef = useCallback(node => {
setRefState(prevRefState => ({
...prevRefState,
[node.dataset.refkey]: node
}))
}, [])
const animationTimeline = useClapAnimation({
clapEl: clapRef,
countEl: clapCountRef,
clapTotalEl: clapTotalRef
})
const componentJustMounted = useRef(true)
useEffect(() => {
if (!componentJustMounted.current) {
console.log('onClap was called!!!')
onClap && onClap(clapState)
}
componentJustMounted.current = false
}, [count])
const handleClapClick = () => {
animationTimeline.replay()
setClapState(prevState => ({
isClicked: true,
count: Math.min(count + 1, MAXIMUM_USER_CLAP),
countTotal:
count < MAXIMUM_USER_CLAP
? prevState.countTotal + 1
: prevState.countTotal
}))
}
const memoizedValue = useMemo(
() => ({
...clapState,
setRef
}),
[clapState, setRef]
)
const classNames = [styles.clap, className].join(' ').trim()
return (
<Provider value={memoizedValue}>
<button
ref={setRef}
data-refkey='clapRef'
className={classNames}
onClick={handleClapClick}
style={userStyles}
>
{children}
</button>
</Provider>
)
}
/**
* subcomponents
*/
const ClapIcon = ({ style: userStyles = {}, className }) => {
const { isClicked } = useContext(MediumClapContext)
const classNames = [styles.icon, isClicked ? styles.checked : '', className]
.join(' ')
.trim()
return (
<span>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='-549 338 100.1 125'
className={classNames}
style={userStyles}
>
<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' />
<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' />
</svg>
</span>
)
}
const ClapCount = ({ style: userStyles = {}, className }) => {
const { count, setRef } = useContext(MediumClapContext)
const classNames = [styles.count, className].join(' ').trim()
return (
<span
ref={setRef}
data-refkey='clapCountRef'
className={classNames}
style={userStyles}
>
+ {count}
</span>
)
}
const CountTotal = ({ style: userStyles = {}, className }) => {
const { countTotal, setRef } = useContext(MediumClapContext)
const classNames = [styles.total, className].join(' ').trim()
return (
<span
ref={setRef}
data-refkey='clapTotalRef'
className={classNames}
style={userStyles}
>
{countTotal}
</span>
)
}
MediumClap.Icon = ClapIcon
MediumClap.Count = ClapCount
MediumClap.Total = CountTotal
/**
* Usage
*/
// import MediumClap from 'medium-clap'
const Usage = () => {
const [count, setCount] = useState(0)
const handleClap = clapState => {
setCount(clapState.count)
}
return (
<div style={{ width: '100%' }}>
<MediumClap onClap={handleClap} className={userCustomStyles.clap}>
<MediumClap.Icon className={userCustomStyles.icon} />
<MediumClap.Count className={userCustomStyles.count} />
<MediumClap.Total className={userCustomStyles.total} />
</MediumClap>
{!!count && (
<div className={styles.info}>{`You have clapped ${count} times`}</div>
)}
</div>
)
}
export default Usage
Se trata de conseguir algo como ocurre con los <input>
, donde tenemos un <input value={...} onChange={...}>
, es decir, controlamos el valor y la acción ante un cambio del valor.
En nuestro componente, no tenemos forma de dar un valor a los elementos del estado del componente. Para ello usaremos control props.
Lo que hace en el componente, es comprobar si el componente recibe una prop values
y el callback onClap
. Si recibe ambos, se dice entonces que es un controlled component, y en vez de utilizar el estado interno, utilizará los values que recibe como props, y el componente pasará a sus hijos estos values, en vez de su estado.
import React, {
useState,
useLayoutEffect,
useCallback,
createContext,
useMemo,
useContext,
useEffect,
useRef
} from 'react'
import mojs from 'mo-js'
import styles from './index.css'
import userCustomStyles from './usage.css'
const initialState = {
count: 0,
countTotal: 267,
isClicked: false
}
/**
* Custom Hook for animation
*/
const useClapAnimation = ({ clapEl, countEl, clapTotalEl }) => {
const [animationTimeline, setAnimationTimeline] = useState(
() => new mojs.Timeline()
)
useLayoutEffect(() => {
if (!clapEl || !countEl || !clapTotalEl) {
return
}
const tlDuration = 300
const scaleButton = new mojs.Html({
el: clapEl,
duration: tlDuration,
scale: { 1.3: 1 },
easing: mojs.easing.ease.out
})
const triangleBurst = new mojs.Burst({
parent: clapEl,
radius: { 50: 95 },
count: 5,
angle: 30,
children: {
shape: 'polygon',
radius: { 6: 0 },
stroke: 'rgba(211,54,0,0.5)',
strokeWidth: 2,
angle: 210,
delay: 30,
speed: 0.2,
easing: mojs.easing.bezier(0.1, 1, 0.3, 1),
duration: tlDuration
}
})
const circleBurst = new mojs.Burst({
parent: clapEl,
radius: { 50: 75 },
angle: 25,
duration: tlDuration,
children: {
shape: 'circle',
fill: 'rgba(149,165,166,0.5)',
delay: 30,
speed: 0.2,
radius: { 3: 0 },
easing: mojs.easing.bezier(0.1, 1, 0.3, 1)
}
})
const countAnimation = new mojs.Html({
el: countEl,
opacity: { 0: 1 },
y: { 0: -30 },
duration: tlDuration
}).then({
opacity: { 1: 0 },
y: -80,
delay: tlDuration / 2
})
const countTotalAnimation = new mojs.Html({
el: clapTotalEl,
opacity: { 0: 1 },
delay: (3 * tlDuration) / 2,
duration: tlDuration,
y: { 0: -3 }
})
if (typeof clapEl === 'string') {
const clap = document.getElementById('clap')
clap.style.transform = 'scale(1,1)'
} else {
clapEl.style.transform = 'scale(1,1)'
}
const newAnimationTimeline = animationTimeline.add([
scaleButton,
countTotalAnimation,
countAnimation,
triangleBurst,
circleBurst
])
setAnimationTimeline(newAnimationTimeline)
}, [clapEl, countEl, clapTotalEl])
return animationTimeline
}
const MediumClapContext = createContext()
const { Provider } = MediumClapContext
const MediumClap = ({
children,
onClap,
values = null,
style: userStyles = {},
className
}) => {
const MAXIMUM_USER_CLAP = 50
const [clapState, setClapState] = useState(initialState)
const { count } = clapState
const [{ clapRef, clapCountRef, clapTotalRef }, setRefState] = useState({})
const setRef = useCallback(node => {
setRefState(prevRefState => ({
...prevRefState,
[node.dataset.refkey]: node
}))
}, [])
const animationTimeline = useClapAnimation({
clapEl: clapRef,
countEl: clapCountRef,
clapTotalEl: clapTotalRef
})
const componentJustMounted = useRef(true)
useEffect(() => {
if (!componentJustMounted.current && !isControlled) {
onClap && onClap(clapState)
}
componentJustMounted.current = false
}, [count, onClap, isControlled])
// controlled component?
const isControlled = !!values && onClap
const handleClapClick = () => {
animationTimeline.replay()
isControlled
? onClap()
: setClapState(prevState => ({
isClicked: true,
count: Math.min(count + 1, MAXIMUM_USER_CLAP),
countTotal:
count < MAXIMUM_USER_CLAP
? prevState.countTotal + 1
: prevState.countTotal
}))
}
const getState = useCallback(() => (isControlled ? values : clapState), [
isControlled,
values,
clapState
])
const memoizedValue = useMemo(
() => ({
...getState(),
setRef
}),
[getState, setRef]
)
const classNames = [styles.clap, className].join(' ').trim()
return (
<Provider value={memoizedValue}>
<button
ref={setRef}
data-refkey='clapRef'
className={classNames}
onClick={handleClapClick}
style={userStyles}
>
{children}
</button>
</Provider>
)
}
/**
* subcomponents
*/
const ClapIcon = ({ style: userStyles = {}, className }) => {
const { isClicked } = useContext(MediumClapContext)
const classNames = [styles.icon, isClicked ? styles.checked : '', className]
.join(' ')
.trim()
return (
<span>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='-549 338 100.1 125'
className={classNames}
style={userStyles}
>
<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' />
<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' />
</svg>
</span>
)
}
const ClapCount = ({ style: userStyles = {}, className }) => {
const { count, setRef } = useContext(MediumClapContext)
const classNames = [styles.count, className].join(' ').trim()
return (
<span
ref={setRef}
data-refkey='clapCountRef'
className={classNames}
style={userStyles}
>
+ {count}
</span>
)
}
const CountTotal = ({ style: userStyles = {}, className }) => {
const { countTotal, setRef } = useContext(MediumClapContext)
const classNames = [styles.total, className].join(' ').trim()
return (
<span
ref={setRef}
data-refkey='clapTotalRef'
className={classNames}
style={userStyles}
>
{countTotal}
</span>
)
}
MediumClap.Icon = ClapIcon
MediumClap.Count = ClapCount
MediumClap.Total = CountTotal
/**
* Usage
*/
// import MediumClap from 'medium-clap'
const Usage = () => {
const [count, setCount] = useState(0)
const handleClap = clapState => {
setCount(clapState.count)
}
return (
<div style={{ width: '100%' }}>
<MediumClap onClap={handleClap} className={userCustomStyles.clap}>
<MediumClap.Icon className={userCustomStyles.icon} />
<MediumClap.Count className={userCustomStyles.count} />
<MediumClap.Total className={userCustomStyles.total} />
</MediumClap>
{!!count && (
<div className={styles.info}>{`You have clapped ${count} times`}</div>
)}
</div>
)
}
export default Usage
import React, { useState, useLayoutEffect, useCallback } from 'react'
import mojs from 'mo-js'
import styles from './index.css'
const initialState = {
count: 0,
countTotal: 267,
isClicked: false
}
/**
* Custom Hook for animation
*/
const useClapAnimation = ({ clapEl, countEl, clapTotalEl }) => {
const [animationTimeline, setAnimationTimeline] = useState(
() => new mojs.Timeline()
)
useLayoutEffect(() => {
if (!clapEl || !countEl || !clapTotalEl) {
return
}
const tlDuration = 300
const scaleButton = new mojs.Html({
el: clapEl,
duration: tlDuration,
scale: { 1.3: 1 },
easing: mojs.easing.ease.out
})
const triangleBurst = new mojs.Burst({
parent: clapEl,
radius: { 50: 95 },
count: 5,
angle: 30,
children: {
shape: 'polygon',
radius: { 6: 0 },
stroke: 'rgba(211,54,0,0.5)',
strokeWidth: 2,
angle: 210,
delay: 30,
speed: 0.2,
easing: mojs.easing.bezier(0.1, 1, 0.3, 1),
duration: tlDuration
}
})
const circleBurst = new mojs.Burst({
parent: clapEl,
radius: { 50: 75 },
angle: 25,
duration: tlDuration,
children: {
shape: 'circle',
fill: 'rgba(149,165,166,0.5)',
delay: 30,
speed: 0.2,
radius: { 3: 0 },
easing: mojs.easing.bezier(0.1, 1, 0.3, 1)
}
})
const countAnimation = new mojs.Html({
el: countEl,
opacity: { 0: 1 },
y: { 0: -30 },
duration: tlDuration
}).then({
opacity: { 1: 0 },
y: -80,
delay: tlDuration / 2
})
const countTotalAnimation = new mojs.Html({
el: clapTotalEl,
opacity: { 0: 1 },
delay: (3 * tlDuration) / 2,
duration: tlDuration,
y: { 0: -3 }
})
if (typeof clapEl === 'string') {
const clap = document.getElementById('clap')
clap.style.transform = 'scale(1,1)'
} else {
clapEl.style.transform = 'scale(1,1)'
}
const newAnimationTimeline = animationTimeline.add([
scaleButton,
countTotalAnimation,
countAnimation,
triangleBurst,
circleBurst
])
setAnimationTimeline(newAnimationTimeline)
}, [clapEl, countEl, clapTotalEl])
return animationTimeline
}
/**
* useDOMRef Hook
*/
const useDOMRef = () => {
const [DOMRef, setRefState] = useState({})
const setRef = useCallback(node => {
setRefState(prevRefState => ({
...prevRefState,
[node.dataset.refkey]: node
}))
}, [])
return [DOMRef, setRef]
}
const MediumClap = () => {
const MAXIMUM_USER_CLAP = 50
const [clapState, setClapState] = useState(initialState)
const { count, countTotal, isClicked } = clapState
const [{ clapRef, clapCountRef, clapTotalRef }, setRef] = useDOMRef()
const animationTimeline = useClapAnimation({
clapEl: clapRef,
countEl: clapCountRef,
clapTotalEl: clapTotalRef
})
const handleClapClick = () => {
animationTimeline.replay()
setClapState(prevState => ({
isClicked: true,
count: Math.min(count + 1, MAXIMUM_USER_CLAP),
countTotal:
count < MAXIMUM_USER_CLAP
? prevState.countTotal + 1
: prevState.countTotal
}))
}
return (
<button
ref={setRef}
data-refkey='clapRef'
className={styles.clap}
onClick={handleClapClick}
>
<ClapIcon isClicked={isClicked} />
<ClapCount count={count} setRef={setRef} />
<CountTotal countTotal={countTotal} setRef={setRef} />
</button>
)
}
/**
* subcomponents
*/
const ClapIcon = ({ isClicked }) => {
return (
<span>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='-549 338 100.1 125'
className={`${styles.icon} ${isClicked && styles.checked}`}
>
<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' />
<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' />
</svg>
</span>
)
}
const ClapCount = ({ count, setRef }) => {
return (
<span ref={setRef} data-refkey='clapCountRef' className={styles.count}>
+ {count}
</span>
)
}
const CountTotal = ({ countTotal, setRef }) => {
return (
<span ref={setRef} data-refkey='clapTotalRef' className={styles.total}>
{countTotal}
</span>
)
}
/**
* Usage
*/
const Usage = () => {
return <MediumClap />
}
export default Usage
Si nos fijamos, en el método updateClapState
del hook useClapState
, se ha metido un useCallback
, para que las diferentes referencias a este hook compartan el estado.
import React, { useState, useLayoutEffect, useCallback } from 'react'
import mojs from 'mo-js'
import styles from './index.css'
const INITIAL_STATE = {
count: 0,
countTotal: 267,
isClicked: false
}
/**
* Custom Hook for animation
*/
const useClapAnimation = ({ clapEl, countEl, clapTotalEl }) => {
const [animationTimeline, setAnimationTimeline] = useState(
() => new mojs.Timeline()
)
useLayoutEffect(() => {
if (!clapEl || !countEl || !clapTotalEl) {
return
}
const tlDuration = 300
const scaleButton = new mojs.Html({
el: clapEl,
duration: tlDuration,
scale: { 1.3: 1 },
easing: mojs.easing.ease.out
})
const triangleBurst = new mojs.Burst({
parent: clapEl,
radius: { 50: 95 },
count: 5,
angle: 30,
children: {
shape: 'polygon',
radius: { 6: 0 },
stroke: 'rgba(211,54,0,0.5)',
strokeWidth: 2,
angle: 210,
delay: 30,
speed: 0.2,
easing: mojs.easing.bezier(0.1, 1, 0.3, 1),
duration: tlDuration
}
})
const circleBurst = new mojs.Burst({
parent: clapEl,
radius: { 50: 75 },
angle: 25,
duration: tlDuration,
children: {
shape: 'circle',
fill: 'rgba(149,165,166,0.5)',
delay: 30,
speed: 0.2,
radius: { 3: 0 },
easing: mojs.easing.bezier(0.1, 1, 0.3, 1)
}
})
const countAnimation = new mojs.Html({
el: countEl,
opacity: { 0: 1 },
y: { 0: -30 },
duration: tlDuration
}).then({
opacity: { 1: 0 },
y: -80,
delay: tlDuration / 2
})
const countTotalAnimation = new mojs.Html({
el: clapTotalEl,
opacity: { 0: 1 },
delay: (3 * tlDuration) / 2,
duration: tlDuration,
y: { 0: -3 }
})
if (typeof clapEl === 'string') {
const clap = document.getElementById('clap')
clap.style.transform = 'scale(1,1)'
} else {
clapEl.style.transform = 'scale(1,1)'
}
const newAnimationTimeline = animationTimeline.add([
scaleButton,
countTotalAnimation,
countAnimation,
triangleBurst,
circleBurst
])
setAnimationTimeline(newAnimationTimeline)
}, [clapEl, countEl, clapTotalEl])
return animationTimeline
}
/**
* useDOMRef Hook
*/
const useDOMRef = () => {
const [DOMRef, setRefState] = useState({})
const setRef = useCallback(node => {
setRefState(prevRefState => ({
...prevRefState,
[node.dataset.refkey]: node
}))
}, [])
return [DOMRef, setRef]
}
/**
* custom hook for useClapState
*/
const useClapState = (initialState = INITIAL_STATE) => {
const MAXIMUM_USER_CLAP = 50
const [clapState, setClapState] = useState(initialState)
const { count, countTotal } = clapState
const updateClapState = useCallback(() => {
setClapState(({ count, countTotal }) => ({
isClicked: true,
count: Math.min(count + 1, MAXIMUM_USER_CLAP),
countTotal: count < MAXIMUM_USER_CLAP ? countTotal + 1 : countTotal
}))
}, [count, countTotal])
return [clapState, updateClapState]
}
const MediumClap = () => {
const [clapState, updateClapState] = useClapState()
const { count, countTotal, isClicked } = clapState
const [{ clapRef, clapCountRef, clapTotalRef }, setRef] = useDOMRef()
const animationTimeline = useClapAnimation({
clapEl: clapRef,
countEl: clapCountRef,
clapTotalEl: clapTotalRef
})
const handleClapClick = () => {
animationTimeline.replay()
updateClapState()
}
return (
<button
ref={setRef}
data-refkey='clapRef'
className={styles.clap}
onClick={handleClapClick}
>
<ClapIcon isClicked={isClicked} />
<ClapCount count={count} setRef={setRef} />
<CountTotal countTotal={countTotal} setRef={setRef} />
</button>
)
}
/**
* subcomponents
*/
const ClapIcon = ({ isClicked }) => {
return (
<span>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='-549 338 100.1 125'
className={`${styles.icon} ${isClicked && styles.checked}`}
>
<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' />
<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' />
</svg>
</span>
)
}
const ClapCount = ({ count, setRef }) => {
return (
<span ref={setRef} data-refkey='clapCountRef' className={styles.count}>
+ {count}
</span>
)
}
const CountTotal = ({ countTotal, setRef }) => {
return (
<span ref={setRef} data-refkey='clapTotalRef' className={styles.total}>
{countTotal}
</span>
)
}
/**
* Usage
*/
const Usage = () => {
return <MediumClap />
}
export default Usage
Vemos como le pasa al hook dos parámetros: cb
o callback, y deps
con el array de dependencias para el useEffect
.
import React, {
useState,
useLayoutEffect,
useCallback,
useRef,
useEffect
} from 'react'
import mojs from 'mo-js'
import styles from './index.css'
const INITIAL_STATE = {
count: 0,
countTotal: 267,
isClicked: false
}
/**
* Custom Hook for animation
*/
const useClapAnimation = ({ clapEl, countEl, clapTotalEl }) => {
const [animationTimeline, setAnimationTimeline] = useState(
() => new mojs.Timeline()
)
useLayoutEffect(() => {
if (!clapEl || !countEl || !clapTotalEl) {
return
}
const tlDuration = 300
const scaleButton = new mojs.Html({
el: clapEl,
duration: tlDuration,
scale: { 1.3: 1 },
easing: mojs.easing.ease.out
})
const triangleBurst = new mojs.Burst({
parent: clapEl,
radius: { 50: 95 },
count: 5,
angle: 30,
children: {
shape: 'polygon',
radius: { 6: 0 },
stroke: 'rgba(211,54,0,0.5)',
strokeWidth: 2,
angle: 210,
delay: 30,
speed: 0.2,
easing: mojs.easing.bezier(0.1, 1, 0.3, 1),
duration: tlDuration
}
})
const circleBurst = new mojs.Burst({
parent: clapEl,
radius: { 50: 75 },
angle: 25,
duration: tlDuration,
children: {
shape: 'circle',
fill: 'rgba(149,165,166,0.5)',
delay: 30,
speed: 0.2,
radius: { 3: 0 },
easing: mojs.easing.bezier(0.1, 1, 0.3, 1)
}
})
const countAnimation = new mojs.Html({
el: countEl,
opacity: { 0: 1 },
y: { 0: -30 },
duration: tlDuration
}).then({
opacity: { 1: 0 },
y: -80,
delay: tlDuration / 2
})
const countTotalAnimation = new mojs.Html({
el: clapTotalEl,
opacity: { 0: 1 },
delay: (3 * tlDuration) / 2,
duration: tlDuration,
y: { 0: -3 }
})
if (typeof clapEl === 'string') {
const clap = document.getElementById('clap')
clap.style.transform = 'scale(1,1)'
} else {
clapEl.style.transform = 'scale(1,1)'
}
const newAnimationTimeline = animationTimeline.add([
scaleButton,
countTotalAnimation,
countAnimation,
triangleBurst,
circleBurst
])
setAnimationTimeline(newAnimationTimeline)
}, [clapEl, countEl, clapTotalEl])
return animationTimeline
}
/**
* useDOMRef Hook
*/
const useDOMRef = () => {
const [DOMRef, setRefState] = useState({})
const setRef = useCallback(node => {
setRefState(prevRefState => ({
...prevRefState,
[node.dataset.refkey]: node
}))
}, [])
return [DOMRef, setRef]
}
/**
* custom hook for useClapState
*/
const useClapState = (initialState = INITIAL_STATE) => {
const MAXIMUM_USER_CLAP = 50
const [clapState, setClapState] = useState(initialState)
const { count, countTotal } = clapState
const updateClapState = useCallback(() => {
setClapState(({ count, countTotal }) => ({
isClicked: true,
count: Math.min(count + 1, MAXIMUM_USER_CLAP),
countTotal: count < MAXIMUM_USER_CLAP ? countTotal + 1 : countTotal
}))
}, [count, countTotal])
return [clapState, updateClapState]
}
/**
* custom useEffectAfterMount hook
*/
const useEffectAfterMount = (cb, deps) => {
const componentJustMounted = useRef(true)
useEffect(() => {
if (!componentJustMounted.current) {
return cb()
}
componentJustMounted.current = false
}, deps)
}
const MediumClap = () => {
const [clapState, updateClapState] = useClapState()
const { count, countTotal, isClicked } = clapState
const [{ clapRef, clapCountRef, clapTotalRef }, setRef] = useDOMRef()
const animationTimeline = useClapAnimation({
clapEl: clapRef,
countEl: clapCountRef,
clapTotalEl: clapTotalRef
})
useEffectAfterMount(() => {
animationTimeline.replay()
}, [count])
return (
<button
ref={setRef}
data-refkey='clapRef'
className={styles.clap}
onClick={updateClapState}
>
<ClapIcon isClicked={isClicked} />
<ClapCount count={count} setRef={setRef} />
<CountTotal countTotal={countTotal} setRef={setRef} />
</button>
)
}
/**
* subcomponents
*/
const ClapIcon = ({ isClicked }) => {
return (
<span>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='-549 338 100.1 125'
className={`${styles.icon} ${isClicked && styles.checked}`}
>
<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' />
<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' />
</svg>
</span>
)
}
const ClapCount = ({ count, setRef }) => {
return (
<span ref={setRef} data-refkey='clapCountRef' className={styles.count}>
+ {count}
</span>
)
}
const CountTotal = ({ countTotal, setRef }) => {
return (
<span ref={setRef} data-refkey='clapTotalRef' className={styles.total}>
{countTotal}
</span>
)
}
/**
* Usage
*/
const Usage = () => {
return <MediumClap />
}
export default Usage
En este ejemplo, va a usar solo el hook useClapAnimation
pero para un componente totalmente diferente, que no va a utilizar ninguno de los otros componentes de nuestra librería.
import React, {
useState,
useEffect,
useCallback,
useLayoutEffect,
useRef
} from 'react'
import mojs from 'mo-js'
import { generateRandomNumber } from '../utils/generateRandomNumber'
import styles from './index.css'
import userStyles from './usage.css'
/** ====================================
* 🔰Hook
Hook for Animation
==================================== **/
const useClapAnimation = ({
duration: tlDuration,
bounceEl,
fadeEl,
burstEl
}) => {
const [animationTimeline, setAnimationTimeline] = useState(
new mojs.Timeline()
)
useLayoutEffect(() => {
if (!bounceEl || !fadeEl || !burstEl) {
return
}
const triangleBurst = new mojs.Burst({
parent: burstEl,
radius: { 50: 95 },
count: 5,
angle: 30,
children: {
shape: 'polygon',
radius: { 6: 0 },
scale: 1,
stroke: 'rgba(211,84,0 ,0.5)',
strokeWidth: 2,
angle: 210,
delay: 30,
speed: 0.2,
easing: mojs.easing.bezier(0.1, 1, 0.3, 1),
duration: tlDuration
}
})
const circleBurst = new mojs.Burst({
parent: burstEl,
radius: { 50: 75 },
angle: 25,
duration: tlDuration,
children: {
shape: 'circle',
fill: 'rgba(149,165,166 ,0.5)',
delay: 30,
speed: 0.2,
radius: { 3: 0 },
easing: mojs.easing.bezier(0.1, 1, 0.3, 1)
}
})
const countAnimation = new mojs.Html({
el: bounceEl,
isShowStart: false,
isShowEnd: true,
y: { 0: -30 },
opacity: { 0: 1 },
duration: tlDuration
}).then({
opacity: { 1: 0 },
y: -80,
delay: tlDuration / 2
})
const countTotalAnimation = new mojs.Html({
el: fadeEl,
isShowStart: false,
isShowEnd: true,
opacity: { 0: 1 },
delay: (3 * tlDuration) / 2,
duration: tlDuration,
y: { 0: -3 }
})
const scaleButton = new mojs.Html({
el: burstEl,
duration: tlDuration,
scale: { 1.3: 1 },
easing: mojs.easing.out
})
if (typeof burstEl === 'string') {
const el = document.getElementById(id)
el.style.transform = 'scale(1, 1)'
} else {
burstEl.style.transform = 'scale(1, 1)'
}
const updatedAnimationTimeline = animationTimeline.add([
countAnimation,
countTotalAnimation,
scaleButton,
circleBurst,
triangleBurst
])
setAnimationTimeline(updatedAnimationTimeline)
}, [tlDuration, animationTimeline, bounceEl, fadeEl, burstEl])
return animationTimeline
}
/** ====================================
* 🔰Hook
Hook for Clap State
==================================== **/
const MAX_CLAP = 50
const INIT_STATE = {
count: 0,
countTotal: generateRandomNumber(500, 10000),
isClicked: false
}
const useClapState = ({ initialState = INIT_STATE } = {}) => {
const [clapState, setClapState] = useState(initialState)
const { count, countTotal } = clapState
const handleClapClick = useCallback(() => {
setClapState({
count: Math.min(count + 1, MAX_CLAP),
countTotal: count < MAX_CLAP ? countTotal + 1 : countTotal,
isClicked: true
})
}, [count, countTotal])
return {
clapState,
handleClapClick
}
}
/** ====================================
* 🔰Hook
useEffectAfterMount
==================================== **/
function useEffectAfterMount (cb, deps) {
const componentJustMounted = useRef(true)
useEffect(() => {
if (!componentJustMounted.current) {
return cb()
}
componentJustMounted.current = false
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps)
}
/** ====================================
* 🔰Hook
useDOMRef
==================================== **/
const useDOMRef = () => {
const [DOMRef, setDOMRef] = useState({})
const setRef = useCallback(node => {
if (node !== null) {
setDOMRef(prevDOMRefs => ({
...prevDOMRefs,
[node.dataset.refkey]: node
}))
}
}, [])
return [DOMRef, setRef]
}
/** ====================================
* 🔰 MediumClap
==================================== **/
const MediumClap = () => {
const { clapState, handleClapClick } = useClapState()
const { count, countTotal, isClicked } = clapState
const [
{ clapContainerRef, clapCountRef, countTotalRef },
setRef
] = useDOMRef()
const animationTimeline = useClapAnimation({
duration: 300,
bounceEl: clapCountRef,
fadeEl: countTotalRef,
burstEl: clapContainerRef
})
useEffectAfterMount(() => {
animationTimeline.replay()
}, [count])
return (
<button
ref={setRef}
data-refkey='clapRef'
className={styles.clap}
onClick={handleClapClick}
>
<ClapIcon isClicked={isClicked} />
<ClapCount count={count} setRef={setRef} />
<CountTotal countTotal={countTotal} setRef={setRef} />
</button>
)
}
/** ====================================
* 🔰SubComponents
Smaller Component used by <MediumClap />
==================================== **/
const ClapIcon = ({ isClicked }) => {
return (
<span>
<svg
id='clapIcon'
xmlns='http://www.w3.org/2000/svg'
viewBox='-549 338 100.1 125'
className={`${styles.icon} ${isClicked && styles.checked}`}
>
<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' />
<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' />
</svg>
</span>
)
}
const ClapCount = ({ count, setRef }) => {
return (
<span ref={setRef} data-refkey='clapCountRef' className={styles.count}>
+{count}
</span>
)
}
const CountTotal = ({ countTotal, setRef }) => {
return (
<span ref={setRef} data-refkey='clapTotalRef' className={styles.total}>
{countTotal}
</span>
)
}
/** ====================================
* 🔰USAGE
Below's how a potential user
may consume the component API
==================================== **/
const CupBowl = () => {
// Credit: Created by Kieu Thi Kim Cuong from the Noun Project
return (
<svg
id='cupBowl'
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 26.09 13.71'
>
<g>
<path d='M26.06.36A.54.54 0 0 0 26 .18h-.05l-.1-.07a.32.32 0 0 0-.09 0H.5A.5.5 0 0 0 0 .5v.06c.09.69.21 1.38.35 2.07a.65.65 0 0 1 0 .53.65.65 0 0 1 .07.56.7.7 0 0 1 0 .76 6.18 6.18 0 0 1 .35 1.66.6.6 0 0 1 .14.45.6.6 0 0 1 .09.32.64.64 0 0 1 .33.79 5.94 5.94 0 0 1 1.1 2.84.48.48 0 0 1 .38.18.58.58 0 0 1 .4.16.58.58 0 0 1 .36.36h.06c.27.45.55.9.85 1.33a2.54 2.54 0 0 0 2.1 1.1h12.85a3 3 0 0 0 .73-.11.51.51 0 0 0 .7-.1c.2-.27.38-.55.57-.82a.34.34 0 0 0 .08-.09c.06-.09.12-.19.19-.28l.41-.65c.31-.48.6-1 .87-1.48l.41-.79c.25-.5.48-1 .7-1.52l.33-.79c.1-.26.19-.53.29-.79s.19-.53.27-.81.17-.56.25-.84.15-.52.22-.78.15-.66.22-1 .1-.43.14-.65c.1-.55.19-1.11.26-1.67a.44.44 0 0 0-.01-.14z' />
</g>
</svg>
)
}
const CupHandle = () => {
// Credit: Created by Kieu Thi Kim Cuong from the Noun Project
return (
<svg
id='cupHandle'
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 7.59 7.81'
>
<g>
<path
d='M2.19 6.08C1.09 5.21.19 3.62.6 2.29A2.66 2.66 0 0 1 2.36.55a3.8 3.8 0 0 1 1.82.2 27.34 27.34 0 0 0 2.55 6.53 7.33 7.33 0 0 1-4.54-1.2z'
fill='none'
stroke='#000'
strokeMiterlimit='10'
/>
</g>
</svg>
)
}
const Stream = () => {
// Credit: Created by Kieu Thi Kim Cuong from the Noun Project
return (
<svg
id='stream'
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 10.03 22.44'
>
<g>
<path d='M7.53 22.35a.5.5 0 0 0 .69-.13A10 10 0 0 0 8 10.43a5.14 5.14 0 0 1 1.52-7.51.49.49 0 0 0 .13-.69.5.5 0 0 0-.65-.17 6.14 6.14 0 0 0-1.8 9 9 9 0 0 1 .2 10.59.5.5 0 0 0 .12.69zM1.54 20.35a.5.5 0 0 0 .69-.12A10 10 0 0 0 2 8.44a5.15 5.15 0 0 1 1-7.2c.15-.11.31-.21.47-.31a.5.5 0 0 0 .24-.68A.51.51 0 0 0 3 .07 6.15 6.15 0 0 0 .85 8.48c.12.19.24.38.38.56a9 9 0 0 1 .18 10.61.51.51 0 0 0 .13.7z' />
</g>
</svg>
)
}
const CupBase = () => {
// Credit: Created by Kieu Thi Kim Cuong from the Noun Project
return (
<svg id='cupBase' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 21.76 1'>
<g>
<path d='M21.26 0H.5a.5.5 0 0 0-.5.5.5.5 0 0 0 .5.5h20.76a.5.5 0 0 0 .5-.5.5.5 0 0 0-.5-.5z' />
</g>
</svg>
)
}
const Usage = () => {
const animationTimeline = useClapAnimation({
duration: 300,
bounceEl: '#stream',
fadeEl: '#cupHandle',
burstEl: '#coffee'
})
const handleClick = () => {
animationTimeline.replay()
}
return (
<section className={userStyles.cupContainer}>
<div className={userStyles.cupStream}>
<Stream />
</div>
<div id='coffee' style={{ fontSize: '0.5rem' }}>
coffee
</div>
<div className={userStyles.cupBody}>
<CupHandle />
<CupBowl />
</div>
<div>
<CupBase />
</div>
<footer>
<button onClick={handleClick}>Animate</button>
</footer>
</section>
)
}
export default Usage
Se pueden pasar las props como parámetros rest.
const ClapCount = ({count, setRef, ...restProps}) => {
return (
<span
ref={setRef}
className={styles.count}
{...restProps}>
hello
</span>
)
}
Las props collection tratan de agrupar las props en función de funcionalidad. Si para gestionar la cuenta del número de clicks tenemos dos props (el valor del count y el callback para cambiarlo), lo podemos agrupar en una prop collection.
Esto permitiría también llevarnos estas props collections y aplicarlas en otro componente, dándole a ese componente ese comportamiento.
En el siguiente ejemplo se ha agrupado en togglerProps
y counterProps
, y en ambos se han añadido props de accesibilidad.
import React, {
useState,
useLayoutEffect,
useCallback,
useRef,
useEffect
} from 'react'
import mojs from 'mo-js'
import styles from './index.css'
const INITIAL_STATE = {
count: 0,
countTotal: 267,
isClicked: false
}
/**
* Custom Hook for animation
*/
const useClapAnimation = ({ clapEl, countEl, clapTotalEl }) => {
const [animationTimeline, setAnimationTimeline] = useState(
() => new mojs.Timeline()
)
useLayoutEffect(() => {
if (!clapEl || !countEl || !clapTotalEl) {
return
}
const tlDuration = 300
const scaleButton = new mojs.Html({
el: clapEl,
duration: tlDuration,
scale: { 1.3: 1 },
easing: mojs.easing.ease.out
})
const triangleBurst = new mojs.Burst({
parent: clapEl,
radius: { 50: 95 },
count: 5,
angle: 30,
children: {
shape: 'polygon',
radius: { 6: 0 },
stroke: 'rgba(211,54,0,0.5)',
strokeWidth: 2,
angle: 210,
delay: 30,
speed: 0.2,
easing: mojs.easing.bezier(0.1, 1, 0.3, 1),
duration: tlDuration
}
})
const circleBurst = new mojs.Burst({
parent: clapEl,
radius: { 50: 75 },
angle: 25,
duration: tlDuration,
children: {
shape: 'circle',
fill: 'rgba(149,165,166,0.5)',
delay: 30,
speed: 0.2,
radius: { 3: 0 },
easing: mojs.easing.bezier(0.1, 1, 0.3, 1)
}
})
const countAnimation = new mojs.Html({
el: countEl,
opacity: { 0: 1 },
y: { 0: -30 },
duration: tlDuration
}).then({
opacity: { 1: 0 },
y: -80,
delay: tlDuration / 2
})
const countTotalAnimation = new mojs.Html({
el: clapTotalEl,
opacity: { 0: 1 },
delay: (3 * tlDuration) / 2,
duration: tlDuration,
y: { 0: -3 }
})
if (typeof clapEl === 'string') {
const clap = document.getElementById('clap')
clap.style.transform = 'scale(1,1)'
} else {
clapEl.style.transform = 'scale(1,1)'
}
const newAnimationTimeline = animationTimeline.add([
scaleButton,
countTotalAnimation,
countAnimation,
triangleBurst,
circleBurst
])
setAnimationTimeline(newAnimationTimeline)
}, [clapEl, countEl, clapTotalEl])
return animationTimeline
}
/**
* useDOMRef Hook
*/
const useDOMRef = () => {
const [DOMRef, setRefState] = useState({})
const setRef = useCallback(node => {
setRefState(prevRefState => ({
...prevRefState,
[node.dataset.refkey]: node
}))
}, [])
return [DOMRef, setRef]
}
/**
* custom hook for useClapState
*/
const useClapState = (initialState = INITIAL_STATE) => {
const MAXIMUM_USER_CLAP = 50
const [clapState, setClapState] = useState(initialState)
const { count, countTotal } = clapState
const updateClapState = useCallback(() => {
setClapState(({ count, countTotal }) => ({
isClicked: true,
count: Math.min(count + 1, MAXIMUM_USER_CLAP),
countTotal: count < MAXIMUM_USER_CLAP ? countTotal + 1 : countTotal
}))
}, [count, countTotal])
// props collection for 'click'
const togglerProps = {
onClick: updateClapState,
'aria-pressed': clapState.isClicked
}
// props collection for 'count'
const counterProps = {
count,
'aria-valuemax': MAXIMUM_USER_CLAP,
'aria-valuemin': 0,
'aria-valuenow': count
}
return { clapState, updateClapState, togglerProps, counterProps }
}
/**
* custom useEffectAfterMount hook
*/
const useEffectAfterMount = (cb, deps) => {
const componentJustMounted = useRef(true)
useEffect(() => {
if (!componentJustMounted.current) {
return cb()
}
componentJustMounted.current = false
}, deps)
}
/**
* subcomponents
*/
const ClapContainer = ({ children, setRef, handleClick, ...restProps }) => {
return (
<button
ref={setRef}
className={styles.clap}
onClick={handleClick}
{...restProps}
>
{children}
</button>
)
}
const ClapIcon = ({ isClicked }) => {
return (
<span>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='-549 338 100.1 125'
className={`${styles.icon} ${isClicked && styles.checked}`}
>
<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' />
<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' />
</svg>
</span>
)
}
const ClapCount = ({ count, setRef, ...restProps }) => {
return (
<span ref={setRef} className={styles.count} {...restProps}>
+ {count}
</span>
)
}
const CountTotal = ({ countTotal, setRef, ...restProps }) => {
return (
<span ref={setRef} className={styles.total} {...restProps}>
{countTotal}
</span>
)
}
/**
* Usage
*/
const Usage = () => {
const {
clapState,
updateClapState,
togglerProps,
counterProps
} = useClapState()
const { count, countTotal, isClicked } = clapState
const [{ clapRef, clapCountRef, clapTotalRef }, setRef] = useDOMRef()
const animationTimeline = useClapAnimation({
clapEl: clapRef,
countEl: clapCountRef,
clapTotalEl: clapTotalRef
})
useEffectAfterMount(() => {
animationTimeline.replay()
}, [count])
return (
<ClapContainer setRef={setRef} data-refkey='clapRef' {...togglerProps}>
{/* <ClapIcon isClicked={isClicked} /> */}
🇳🇬
<ClapCount setRef={setRef} data-refkey='clapCountRef' {...counterProps} />
<CountTotal
countTotal={countTotal}
setRef={setRef}
data-refkey='clapTotalRef'
/>
</ClapContainer>
)
}
export default Usage
Aquí básicamente lo que hace es convertir los objetos donde se definen las props collections en funciones. Así, togglerProps
por ejemplo se convierte en getTogglerProps()
.
import React, {
useState,
useLayoutEffect,
useCallback,
useRef,
useEffect
} from 'react'
import mojs from 'mo-js'
import styles from './index.css'
const INITIAL_STATE = {
count: 0,
countTotal: 267,
isClicked: false
}
/**
* Custom Hook for animation
*/
const useClapAnimation = ({ clapEl, countEl, clapTotalEl }) => {
const [animationTimeline, setAnimationTimeline] = useState(
() => new mojs.Timeline()
)
useLayoutEffect(() => {
if (!clapEl || !countEl || !clapTotalEl) {
return
}
const tlDuration = 300
const scaleButton = new mojs.Html({
el: clapEl,
duration: tlDuration,
scale: { 1.3: 1 },
easing: mojs.easing.ease.out
})
const triangleBurst = new mojs.Burst({
parent: clapEl,
radius: { 50: 95 },
count: 5,
angle: 30,
children: {
shape: 'polygon',
radius: { 6: 0 },
stroke: 'rgba(211,54,0,0.5)',
strokeWidth: 2,
angle: 210,
delay: 30,
speed: 0.2,
easing: mojs.easing.bezier(0.1, 1, 0.3, 1),
duration: tlDuration
}
})
const circleBurst = new mojs.Burst({
parent: clapEl,
radius: { 50: 75 },
angle: 25,
duration: tlDuration,
children: {
shape: 'circle',
fill: 'rgba(149,165,166,0.5)',
delay: 30,
speed: 0.2,
radius: { 3: 0 },
easing: mojs.easing.bezier(0.1, 1, 0.3, 1)
}
})
const countAnimation = new mojs.Html({
el: countEl,
opacity: { 0: 1 },
y: { 0: -30 },
duration: tlDuration
}).then({
opacity: { 1: 0 },
y: -80,
delay: tlDuration / 2
})
const countTotalAnimation = new mojs.Html({
el: clapTotalEl,
opacity: { 0: 1 },
delay: (3 * tlDuration) / 2,
duration: tlDuration,
y: { 0: -3 }
})
if (typeof clapEl === 'string') {
const clap = document.getElementById('clap')
clap.style.transform = 'scale(1,1)'
} else {
clapEl.style.transform = 'scale(1,1)'
}
const newAnimationTimeline = animationTimeline.add([
scaleButton,
countTotalAnimation,
countAnimation,
triangleBurst,
circleBurst
])
setAnimationTimeline(newAnimationTimeline)
}, [clapEl, countEl, clapTotalEl])
return animationTimeline
}
/**
* useDOMRef Hook
*/
const useDOMRef = () => {
const [DOMRef, setRefState] = useState({})
const setRef = useCallback(node => {
setRefState(prevRefState => ({
...prevRefState,
[node.dataset.refkey]: node
}))
}, [])
return [DOMRef, setRef]
}
// const handleClick = (evt) => { ... }
// <button onClick={handleClick} />
const callFnsInSequence = (...fns) => (...args) => {
fns.forEach(fn => fn && fn(...args))
}
/**
* custom hook for useClapState
*/
const useClapState = (initialState = INITIAL_STATE) => {
const MAXIMUM_USER_CLAP = 50
const [clapState, setClapState] = useState(initialState)
const { count, countTotal } = clapState
const updateClapState = useCallback(() => {
setClapState(({ count, countTotal }) => ({
isClicked: true,
count: Math.min(count + 1, MAXIMUM_USER_CLAP),
countTotal: count < MAXIMUM_USER_CLAP ? countTotal + 1 : countTotal
}))
}, [count, countTotal])
const getTogglerProps = ({ onClick, ...otherProps } = {}) => ({
onClick: callFnsInSequence(updateClapState, onClick),
'aria-pressed': clapState.isClicked,
...otherProps
})
const getCounterProps = ({ ...otherProps }) => ({
count,
'aria-valuemax': MAXIMUM_USER_CLAP,
'aria-valuemin': 0,
'aria-valuenow': count,
...otherProps
})
return { clapState, updateClapState, getTogglerProps, getCounterProps }
}
/**
* custom useEffectAfterMount hook
*/
const useEffectAfterMount = (cb, deps) => {
const componentJustMounted = useRef(true)
useEffect(() => {
if (!componentJustMounted.current) {
return cb()
}
componentJustMounted.current = false
}, deps)
}
/**
* subcomponents
*/
const ClapContainer = ({ children, setRef, handleClick, ...restProps }) => {
return (
<button
ref={setRef}
className={styles.clap}
onClick={handleClick}
{...restProps}
>
{children}
</button>
)
}
const ClapIcon = ({ isClicked }) => {
return (
<span>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='-549 338 100.1 125'
className={`${styles.icon} ${isClicked && styles.checked}`}
>
<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' />
<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' />
</svg>
</span>
)
}
const ClapCount = ({ count, setRef, ...restProps }) => {
return (
<span ref={setRef} className={styles.count} {...restProps}>
+ {count}
</span>
)
}
const CountTotal = ({ countTotal, setRef, ...restProps }) => {
return (
<span ref={setRef} className={styles.total} {...restProps}>
{countTotal}
</span>
)
}
/**
* Usage
*/
const Usage = () => {
const {
clapState,
updateClapState,
getTogglerProps,
getCounterProps
} = useClapState()
const { count, countTotal, isClicked } = clapState
const [{ clapRef, clapCountRef, clapTotalRef }, setRef] = useDOMRef()
const animationTimeline = useClapAnimation({
clapEl: clapRef,
countEl: clapCountRef,
clapTotalEl: clapTotalRef
})
useEffectAfterMount(() => {
animationTimeline.replay()
}, [count])
const handleClick = () => {
console.log('CLICKED!!!!')
}
return (
<ClapContainer
setRef={setRef}
data-refkey='clapRef'
{...getTogglerProps({
onClick: handleClick,
'aria-pressed': false
})}
>
{/* <ClapIcon isClicked={isClicked} /> */}
🇳🇬
<ClapCount
setRef={setRef}
data-refkey='clapCountRef'
{...getCounterProps()}
/>
<CountTotal
countTotal={countTotal}
setRef={setRef}
data-refkey='clapTotalRef'
/>
</ClapContainer>
)
}
export default Usage
¿Para qué sirve lo que hemos hecho en el punto anterior?
La gracia está en que esto abre la posibilidad de que quien utilice nuestra librería pueda cambiarle, por ejemplo, lo que se ejecuta en la prop onClick
.
Sería algo como lo siguiente:
Por un lado definimos la prop collection:
const getTogglerProps = ({...otherProps}) => ({
onClick: updateClapState,
'aria-pressed': clapState.isClicked,
...otherProps
})
El poder recibir otherProps
hace que se pueda pasar reimplementaciones de las props, que sobreescribirán las existentes.
<ClapContainer
setRef={setRef}
data-refkey='clapRef'
{...getTogglerProps({
onClick: otherHandlerClick,
'aria-pressed': false
})}
>
Aquí por ejemplo estamos sobreescribiendo ambas props.
¿Y si queremos que se ejecute el onClick por defecto y además el nuestro?
Lo que podemos hacer es en primer lugar hacer destructuring de la prop y luego ejecutar ambas funciones en serie:
const getTogglerProps = ({onClick, ...otherProps} = {}) => ({
onClick: callFnsInSequence(updateClapState, onClick),
'aria-pressed': clapState.isClicked,
...otherProps
})
Y luego nos definimos la función que ejecutará funciones en serie:
const callFunctionsInSequence = (...fns) =>
(...args) =>
fns.forEach(fn => fn && fn(...args))
La primera invocación recibe las funciones como parámetros (...fns)
, y la segunda toma los argumentos que se hayan pasado a la función (...args)
.
En este ejemplo, la implementación es:
import React, {
useState,
useLayoutEffect,
useCallback,
useRef,
useEffect
} from 'react'
import mojs from 'mo-js'
import styles from './index.css'
const INITIAL_STATE = {
count: 0,
countTotal: 267,
isClicked: false
}
/**
* Custom Hook for animation
*/
const useClapAnimation = ({ clapEl, countEl, clapTotalEl }) => {
const [animationTimeline, setAnimationTimeline] = useState(
() => new mojs.Timeline()
)
useLayoutEffect(() => {
if (!clapEl || !countEl || !clapTotalEl) {
return
}
const tlDuration = 300
const scaleButton = new mojs.Html({
el: clapEl,
duration: tlDuration,
scale: { 1.3: 1 },
easing: mojs.easing.ease.out
})
const triangleBurst = new mojs.Burst({
parent: clapEl,
radius: { 50: 95 },
count: 5,
angle: 30,
children: {
shape: 'polygon',
radius: { 6: 0 },
stroke: 'rgba(211,54,0,0.5)',
strokeWidth: 2,
angle: 210,
delay: 30,
speed: 0.2,
easing: mojs.easing.bezier(0.1, 1, 0.3, 1),
duration: tlDuration
}
})
const circleBurst = new mojs.Burst({
parent: clapEl,
radius: { 50: 75 },
angle: 25,
duration: tlDuration,
children: {
shape: 'circle',
fill: 'rgba(149,165,166,0.5)',
delay: 30,
speed: 0.2,
radius: { 3: 0 },
easing: mojs.easing.bezier(0.1, 1, 0.3, 1)
}
})
const countAnimation = new mojs.Html({
el: countEl,
opacity: { 0: 1 },
y: { 0: -30 },
duration: tlDuration
}).then({
opacity: { 1: 0 },
y: -80,
delay: tlDuration / 2
})
const countTotalAnimation = new mojs.Html({
el: clapTotalEl,
opacity: { 0: 1 },
delay: (3 * tlDuration) / 2,
duration: tlDuration,
y: { 0: -3 }
})
if (typeof clapEl === 'string') {
const clap = document.getElementById('clap')
clap.style.transform = 'scale(1,1)'
} else {
clapEl.style.transform = 'scale(1,1)'
}
const newAnimationTimeline = animationTimeline.add([
scaleButton,
countTotalAnimation,
countAnimation,
triangleBurst,
circleBurst
])
setAnimationTimeline(newAnimationTimeline)
}, [clapEl, countEl, clapTotalEl])
return animationTimeline
}
/**
* useDOMRef Hook
*/
const useDOMRef = () => {
const [DOMRef, setRefState] = useState({})
const setRef = useCallback(node => {
setRefState(prevRefState => ({
...prevRefState,
[node.dataset.refkey]: node
}))
}, [])
return [DOMRef, setRef]
}
// const handleClick = (evt) => { ... }
// <button onClick={handleClick} />
const callFnsInSequence = (...fns) => (...args) => {
fns.forEach(fn => fn && fn(...args))
}
/**
* custom hook for useClapState
*/
const useClapState = (initialState = INITIAL_STATE) => {
const MAXIMUM_USER_CLAP = 50
const [clapState, setClapState] = useState(initialState)
const { count, countTotal } = clapState
const updateClapState = useCallback(() => {
setClapState(({ count, countTotal }) => ({
isClicked: true,
count: Math.min(count + 1, MAXIMUM_USER_CLAP),
countTotal: count < MAXIMUM_USER_CLAP ? countTotal + 1 : countTotal
}))
}, [count, countTotal])
const getTogglerProps = ({ onClick, ...otherProps } = {}) => ({
onClick: callFnsInSequence(updateClapState, onClick),
'aria-pressed': clapState.isClicked,
...otherProps
})
const getCounterProps = ({ ...otherProps }) => ({
count,
'aria-valuemax': MAXIMUM_USER_CLAP,
'aria-valuemin': 0,
'aria-valuenow': count,
...otherProps
})
return { clapState, updateClapState, getTogglerProps, getCounterProps }
}
/**
* custom useEffectAfterMount hook
*/
const useEffectAfterMount = (cb, deps) => {
const componentJustMounted = useRef(true)
useEffect(() => {
if (!componentJustMounted.current) {
return cb()
}
componentJustMounted.current = false
}, deps)
}
/**
* subcomponents
*/
const ClapContainer = ({ children, setRef, handleClick, ...restProps }) => {
return (
<button
ref={setRef}
className={styles.clap}
onClick={handleClick}
{...restProps}
>
{children}
</button>
)
}
const ClapIcon = ({ isClicked }) => {
return (
<span>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='-549 338 100.1 125'
className={`${styles.icon} ${isClicked && styles.checked}`}
>
<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' />
<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' />
</svg>
</span>
)
}
const ClapCount = ({ count, setRef, ...restProps }) => {
return (
<span ref={setRef} className={styles.count} {...restProps}>
+ {count}
</span>
)
}
const CountTotal = ({ countTotal, setRef, ...restProps }) => {
return (
<span ref={setRef} className={styles.total} {...restProps}>
{countTotal}
</span>
)
}
/**
* Usage
*/
const Usage = () => {
const {
clapState,
updateClapState,
getTogglerProps,
getCounterProps
} = useClapState()
const { count, countTotal, isClicked } = clapState
const [{ clapRef, clapCountRef, clapTotalRef }, setRef] = useDOMRef()
const animationTimeline = useClapAnimation({
clapEl: clapRef,
countEl: clapCountRef,
clapTotalEl: clapTotalRef
})
useEffectAfterMount(() => {
animationTimeline.replay()
}, [count])
const handleClick = () => {
console.log('CLICKED!!!!')
}
return (
<ClapContainer
setRef={setRef}
data-refkey='clapRef'
{...getTogglerProps({
onClick: handleClick,
'aria-pressed': false
})}
>
{/* <ClapIcon isClicked={isClicked} /> */}
🇳🇬
<ClapCount
setRef={setRef}
data-refkey='clapCountRef'
{...getCounterProps()}
/>
<CountTotal
countTotal={countTotal}
setRef={setRef}
data-refkey='clapTotalRef'
/>
</ClapContainer>
)
}
export default Usage
Este patrón consiste en que se debe posibilitar, para un componente, darle su estado inicial, y también la opción de hacer un reset del estado, para dejarlo en el estado inicial.
En el siguiente ejemplo, se provee de un estado inicial y de un reset, que son usados por un componente aparte.
import React, {
useState,
useLayoutEffect,
useCallback,
useRef,
useEffect
} from 'react'
import mojs from 'mo-js'
import styles from './index.css'
import userStyles from './usage.css'
const INITIAL_STATE = {
count: 0,
countTotal: 267,
isClicked: false
}
/**
* Custom Hook for animation
*/
const useClapAnimation = ({ clapEl, countEl, clapTotalEl }) => {
const [animationTimeline, setAnimationTimeline] = useState(
() => new mojs.Timeline()
)
useLayoutEffect(() => {
if (!clapEl || !countEl || !clapTotalEl) {
return
}
const tlDuration = 300
const scaleButton = new mojs.Html({
el: clapEl,
duration: tlDuration,
scale: { 1.3: 1 },
easing: mojs.easing.ease.out
})
const triangleBurst = new mojs.Burst({
parent: clapEl,
radius: { 50: 95 },
count: 5,
angle: 30,
children: {
shape: 'polygon',
radius: { 6: 0 },
stroke: 'rgba(211,54,0,0.5)',
strokeWidth: 2,
angle: 210,
delay: 30,
speed: 0.2,
easing: mojs.easing.bezier(0.1, 1, 0.3, 1),
duration: tlDuration
}
})
const circleBurst = new mojs.Burst({
parent: clapEl,
radius: { 50: 75 },
angle: 25,
duration: tlDuration,
children: {
shape: 'circle',
fill: 'rgba(149,165,166,0.5)',
delay: 30,
speed: 0.2,
radius: { 3: 0 },
easing: mojs.easing.bezier(0.1, 1, 0.3, 1)
}
})
const countAnimation = new mojs.Html({
el: countEl,
opacity: { 0: 1 },
y: { 0: -30 },
duration: tlDuration
}).then({
opacity: { 1: 0 },
y: -80,
delay: tlDuration / 2
})
const countTotalAnimation = new mojs.Html({
el: clapTotalEl,
opacity: { 0: 1 },
delay: (3 * tlDuration) / 2,
duration: tlDuration,
y: { 0: -3 }
})
if (typeof clapEl === 'string') {
const clap = document.getElementById('clap')
clap.style.transform = 'scale(1,1)'
} else {
clapEl.style.transform = 'scale(1,1)'
}
const newAnimationTimeline = animationTimeline.add([
scaleButton,
countTotalAnimation,
countAnimation,
triangleBurst,
circleBurst
])
setAnimationTimeline(newAnimationTimeline)
}, [clapEl, countEl, clapTotalEl])
return animationTimeline
}
/**
* useDOMRef Hook
*/
const useDOMRef = () => {
const [DOMRef, setRefState] = useState({})
const setRef = useCallback(node => {
setRefState(prevRefState => ({
...prevRefState,
[node.dataset.refkey]: node
}))
}, [])
return [DOMRef, setRef]
}
// const handleClick = (evt) => { ... }
// <button onClick={handleClick} />
const callFnsInSequence = (...fns) => (...args) => {
fns.forEach(fn => fn && fn(...args))
}
/**
* custom hook for useClapState
*/
const useClapState = (initialState = INITIAL_STATE) => {
const MAXIMUM_USER_CLAP = 50
const userInitialState = useRef(initialState)
const [clapState, setClapState] = useState(initialState)
const { count, countTotal } = clapState
const updateClapState = useCallback(() => {
setClapState(({ count, countTotal }) => ({
isClicked: true,
count: Math.min(count + 1, MAXIMUM_USER_CLAP),
countTotal: count < MAXIMUM_USER_CLAP ? countTotal + 1 : countTotal
}))
}, [count, countTotal])
const reset = useCallback(() => {
setClapState(userInitialState.current)
}, [setClapState])
const getTogglerProps = ({ onClick, ...otherProps } = {}) => ({
onClick: callFnsInSequence(updateClapState, onClick),
'aria-pressed': clapState.isClicked,
...otherProps
})
const getCounterProps = ({ ...otherProps }) => ({
count,
'aria-valuemax': MAXIMUM_USER_CLAP,
'aria-valuemin': 0,
'aria-valuenow': count,
...otherProps
})
return { clapState, updateClapState, getTogglerProps, getCounterProps, reset }
}
/**
* custom useEffectAfterMount hook
*/
const useEffectAfterMount = (cb, deps) => {
const componentJustMounted = useRef(true)
useEffect(() => {
if (!componentJustMounted.current) {
return cb()
}
componentJustMounted.current = false
}, deps)
}
/**
* subcomponents
*/
const ClapContainer = ({ children, setRef, handleClick, ...restProps }) => {
return (
<button
ref={setRef}
className={styles.clap}
onClick={handleClick}
{...restProps}
>
{children}
</button>
)
}
const ClapIcon = ({ isClicked }) => {
return (
<span>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='-549 338 100.1 125'
className={`${styles.icon} ${isClicked && styles.checked}`}
>
<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' />
<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' />
</svg>
</span>
)
}
const ClapCount = ({ count, setRef, ...restProps }) => {
return (
<span ref={setRef} className={styles.count} {...restProps}>
+ {count}
</span>
)
}
const CountTotal = ({ countTotal, setRef, ...restProps }) => {
return (
<span ref={setRef} className={styles.total} {...restProps}>
{countTotal}
</span>
)
}
/**
* Usage
*/
const userInitialState = {
count: 0,
countTotal: 1000,
isClicked: false
}
const Usage = () => {
const { clapState, getTogglerProps, getCounterProps, reset } = useClapState(
userInitialState
)
const { count, countTotal, isClicked } = clapState
const [{ clapRef, clapCountRef, clapTotalRef }, setRef] = useDOMRef()
const animationTimeline = useClapAnimation({
clapEl: clapRef,
countEl: clapCountRef,
clapTotalEl: clapTotalRef
})
useEffectAfterMount(() => {
animationTimeline.replay()
}, [count])
const handleClick = () => {
console.log('CLICKED!!!!')
}
return (
<div>
<ClapContainer
setRef={setRef}
data-refkey='clapRef'
{...getTogglerProps({
onClick: handleClick,
'aria-pressed': false
})}
>
<ClapIcon isClicked={isClicked} />
<ClapCount
setRef={setRef}
data-refkey='clapCountRef'
{...getCounterProps()}
/>
<CountTotal
countTotal={countTotal}
setRef={setRef}
data-refkey='clapTotalRef'
/>
</ClapContainer>
<section>
<button onClick={reset} className={userStyles.resetBtn}>
reset
</button>
<pre className={userStyles.resetMsg}>
{JSON.stringify({ count, countTotal, isClicked })}
</pre>
</section>
</div>
)
}
export default Usage
Si queremos realizar una acción colateral en el reset, y que solo se aplique cuando haya algo que resetear, a continuación un ejemplo:
import React, {
useState,
useLayoutEffect,
useCallback,
useRef,
useEffect
} from 'react'
import mojs from 'mo-js'
import styles from './index.css'
import userStyles from './usage.css'
const INITIAL_STATE = {
count: 0,
countTotal: 267,
isClicked: false
}
/**
* Custom Hook for animation
*/
const useClapAnimation = ({ clapEl, countEl, clapTotalEl }) => {
const [animationTimeline, setAnimationTimeline] = useState(
() => new mojs.Timeline()
)
useLayoutEffect(() => {
if (!clapEl || !countEl || !clapTotalEl) {
return
}
const tlDuration = 300
const scaleButton = new mojs.Html({
el: clapEl,
duration: tlDuration,
scale: { 1.3: 1 },
easing: mojs.easing.ease.out
})
const triangleBurst = new mojs.Burst({
parent: clapEl,
radius: { 50: 95 },
count: 5,
angle: 30,
children: {
shape: 'polygon',
radius: { 6: 0 },
stroke: 'rgba(211,54,0,0.5)',
strokeWidth: 2,
angle: 210,
delay: 30,
speed: 0.2,
easing: mojs.easing.bezier(0.1, 1, 0.3, 1),
duration: tlDuration
}
})
const circleBurst = new mojs.Burst({
parent: clapEl,
radius: { 50: 75 },
angle: 25,
duration: tlDuration,
children: {
shape: 'circle',
fill: 'rgba(149,165,166,0.5)',
delay: 30,
speed: 0.2,
radius: { 3: 0 },
easing: mojs.easing.bezier(0.1, 1, 0.3, 1)
}
})
const countAnimation = new mojs.Html({
el: countEl,
opacity: { 0: 1 },
y: { 0: -30 },
duration: tlDuration
}).then({
opacity: { 1: 0 },
y: -80,
delay: tlDuration / 2
})
const countTotalAnimation = new mojs.Html({
el: clapTotalEl,
opacity: { 0: 1 },
delay: (3 * tlDuration) / 2,
duration: tlDuration,
y: { 0: -3 }
})
if (typeof clapEl === 'string') {
const clap = document.getElementById('clap')
clap.style.transform = 'scale(1,1)'
} else {
clapEl.style.transform = 'scale(1,1)'
}
const newAnimationTimeline = animationTimeline.add([
scaleButton,
countTotalAnimation,
countAnimation,
triangleBurst,
circleBurst
])
setAnimationTimeline(newAnimationTimeline)
}, [clapEl, countEl, clapTotalEl])
return animationTimeline
}
/**
* useDOMRef Hook
*/
const useDOMRef = () => {
const [DOMRef, setRefState] = useState({})
const setRef = useCallback(node => {
setRefState(prevRefState => ({
...prevRefState,
[node.dataset.refkey]: node
}))
}, [])
return [DOMRef, setRef]
}
/**
*
* custom hook for getting preivous prop/state
*/
const usePrevious = value => {
const ref = useRef()
useEffect(() => {
ref.current = value
})
return ref.current
}
// const handleClick = (evt) => { ... }
// <button onClick={handleClick} />
const callFnsInSequence = (...fns) => (...args) => {
fns.forEach(fn => fn && fn(...args))
}
/**
* custom hook for useClapState
*/
const useClapState = (initialState = INITIAL_STATE) => {
const MAXIMUM_USER_CLAP = 50
const userInitialState = useRef(initialState)
const [clapState, setClapState] = useState(initialState)
const { count, countTotal } = clapState
const updateClapState = useCallback(() => {
setClapState(({ count, countTotal }) => ({
isClicked: true,
count: Math.min(count + 1, MAXIMUM_USER_CLAP),
countTotal: count < MAXIMUM_USER_CLAP ? countTotal + 1 : countTotal
}))
}, [count, countTotal])
// glorified counter
const resetRef = useRef(0)
const prevCount = usePrevious(count)
const reset = useCallback(() => {
if (prevCount !== count) {
setClapState(userInitialState.current)
resetRef.current++
}
}, [prevCount, count, setClapState])
const getTogglerProps = ({ onClick, ...otherProps } = {}) => ({
onClick: callFnsInSequence(updateClapState, onClick),
'aria-pressed': clapState.isClicked,
...otherProps
})
const getCounterProps = ({ ...otherProps }) => ({
count,
'aria-valuemax': MAXIMUM_USER_CLAP,
'aria-valuemin': 0,
'aria-valuenow': count,
...otherProps
})
return {
clapState,
updateClapState,
getTogglerProps,
getCounterProps,
reset,
resetDep: resetRef.current
}
}
/**
* custom useEffectAfterMount hook
*/
const useEffectAfterMount = (cb, deps) => {
const componentJustMounted = useRef(true)
useEffect(() => {
if (!componentJustMounted.current) {
return cb()
}
componentJustMounted.current = false
}, deps)
}
/**
* subcomponents
*/
const ClapContainer = ({ children, setRef, handleClick, ...restProps }) => {
return (
<button
ref={setRef}
className={styles.clap}
onClick={handleClick}
{...restProps}
>
{children}
</button>
)
}
const ClapIcon = ({ isClicked }) => {
return (
<span>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='-549 338 100.1 125'
className={`${styles.icon} ${isClicked && styles.checked}`}
>
<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' />
<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' />
</svg>
</span>
)
}
const ClapCount = ({ count, setRef, ...restProps }) => {
return (
<span ref={setRef} className={styles.count} {...restProps}>
+ {count}
</span>
)
}
const CountTotal = ({ countTotal, setRef, ...restProps }) => {
return (
<span ref={setRef} className={styles.total} {...restProps}>
{countTotal}
</span>
)
}
/**
* Usage
*/
const userInitialState = {
count: 0,
countTotal: 1000,
isClicked: false
}
const Usage = () => {
const {
clapState,
getTogglerProps,
getCounterProps,
reset,
resetDep
} = useClapState(userInitialState)
const { count, countTotal, isClicked } = clapState
const [{ clapRef, clapCountRef, clapTotalRef }, setRef] = useDOMRef()
const animationTimeline = useClapAnimation({
clapEl: clapRef,
countEl: clapCountRef,
clapTotalEl: clapTotalRef
})
useEffectAfterMount(() => {
animationTimeline.replay()
}, [count])
const [uploadingReset, setUpload] = useState(false)
useEffectAfterMount(() => {
setUpload(true)
const id = setTimeout(() => {
setUpload(false)
}, 3000)
return () => clearTimeout(id)
}, [resetDep])
const handleClick = () => {
console.log('CLICKED!!!!')
}
return (
<div>
<ClapContainer
setRef={setRef}
data-refkey='clapRef'
{...getTogglerProps({
onClick: handleClick,
'aria-pressed': false
})}
>
<ClapIcon isClicked={isClicked} />
<ClapCount
setRef={setRef}
data-refkey='clapCountRef'
{...getCounterProps()}
/>
<CountTotal
countTotal={countTotal}
setRef={setRef}
data-refkey='clapTotalRef'
/>
</ClapContainer>
<section>
<button onClick={reset} className={userStyles.resetBtn}>
reset
</button>
<pre className={userStyles.resetMsg}>
{JSON.stringify({ count, countTotal, isClicked })}
</pre>
<pre className={userStyles.resetMsg}>
{uploadingReset ? `uploading reset ${resetDep} ...` : ''}
</pre>
</section>
</div>
)
}
export default Usage
import React, {
useState,
useLayoutEffect,
useCallback,
useRef,
useEffect,
useReducer
} from 'react'
import mojs from 'mo-js'
import styles from './index.css'
import userStyles from './usage.css'
const INITIAL_STATE = {
count: 0,
countTotal: 267,
isClicked: false
}
/**
* Custom Hook for animation
*/
const useClapAnimation = ({ clapEl, countEl, clapTotalEl }) => {
const [animationTimeline, setAnimationTimeline] = useState(
() => new mojs.Timeline()
)
useLayoutEffect(() => {
if (!clapEl || !countEl || !clapTotalEl) {
return
}
const tlDuration = 300
const scaleButton = new mojs.Html({
el: clapEl,
duration: tlDuration,
scale: { 1.3: 1 },
easing: mojs.easing.ease.out
})
const triangleBurst = new mojs.Burst({
parent: clapEl,
radius: { 50: 95 },
count: 5,
angle: 30,
children: {
shape: 'polygon',
radius: { 6: 0 },
stroke: 'rgba(211,54,0,0.5)',
strokeWidth: 2,
angle: 210,
delay: 30,
speed: 0.2,
easing: mojs.easing.bezier(0.1, 1, 0.3, 1),
duration: tlDuration
}
})
const circleBurst = new mojs.Burst({
parent: clapEl,
radius: { 50: 75 },
angle: 25,
duration: tlDuration,
children: {
shape: 'circle',
fill: 'rgba(149,165,166,0.5)',
delay: 30,
speed: 0.2,
radius: { 3: 0 },
easing: mojs.easing.bezier(0.1, 1, 0.3, 1)
}
})
const countAnimation = new mojs.Html({
el: countEl,
opacity: { 0: 1 },
y: { 0: -30 },
duration: tlDuration
}).then({
opacity: { 1: 0 },
y: -80,
delay: tlDuration / 2
})
const countTotalAnimation = new mojs.Html({
el: clapTotalEl,
opacity: { 0: 1 },
delay: (3 * tlDuration) / 2,
duration: tlDuration,
y: { 0: -3 }
})
if (typeof clapEl === 'string') {
const clap = document.getElementById('clap')
clap.style.transform = 'scale(1,1)'
} else {
clapEl.style.transform = 'scale(1,1)'
}
const newAnimationTimeline = animationTimeline.add([
scaleButton,
countTotalAnimation,
countAnimation,
triangleBurst,
circleBurst
])
setAnimationTimeline(newAnimationTimeline)
}, [clapEl, countEl, clapTotalEl])
return animationTimeline
}
/**
* useDOMRef Hook
*/
const useDOMRef = () => {
const [DOMRef, setRefState] = useState({})
const setRef = useCallback(node => {
setRefState(prevRefState => ({
...prevRefState,
[node.dataset.refkey]: node
}))
}, [])
return [DOMRef, setRef]
}
/**
*
* custom hook for getting preivous prop/state
*/
const usePrevious = value => {
const ref = useRef()
useEffect(() => {
ref.current = value
})
return ref.current
}
// const handleClick = (evt) => { ... }
// <button onClick={handleClick} />
const callFnsInSequence = (...fns) => (...args) => {
fns.forEach(fn => fn && fn(...args))
}
/**
* custom hook for useClapState
*/
const MAXIMUM_USER_CLAP = 50
const reducer = ({ count, countTotal }, { type, payload }) => {
switch (type) {
case 'clap':
return {
isClicked: true,
count: Math.min(count + 1, MAXIMUM_USER_CLAP),
countTotal: count < MAXIMUM_USER_CLAP ? countTotal + 1 : countTotal
}
case 'reset':
return payload
default:
break
}
}
const useClapState = (initialState = INITIAL_STATE) => {
const userInitialState = useRef(initialState)
const [clapState, dispatch] = useReducer(reducer, initialState)
const { count, countTotal } = clapState
const updateClapState = () => dispatch({ type: 'clap' })
// glorified counter
const resetRef = useRef(0)
const prevCount = usePrevious(count)
const reset = useCallback(() => {
if (prevCount !== count) {
dispatch({ type: 'reset', payload: userInitialState.current })
resetRef.current++
}
}, [prevCount, count, dispatch])
const getTogglerProps = ({ onClick, ...otherProps } = {}) => ({
onClick: callFnsInSequence(updateClapState, onClick),
'aria-pressed': clapState.isClicked,
...otherProps
})
const getCounterProps = ({ ...otherProps }) => ({
count,
'aria-valuemax': MAXIMUM_USER_CLAP,
'aria-valuemin': 0,
'aria-valuenow': count,
...otherProps
})
return {
clapState,
updateClapState,
getTogglerProps,
getCounterProps,
reset,
resetDep: resetRef.current
}
}
/**
* custom useEffectAfterMount hook
*/
const useEffectAfterMount = (cb, deps) => {
const componentJustMounted = useRef(true)
useEffect(() => {
if (!componentJustMounted.current) {
return cb()
}
componentJustMounted.current = false
}, deps)
}
/**
* subcomponents
*/
const ClapContainer = ({ children, setRef, handleClick, ...restProps }) => {
return (
<button
ref={setRef}
className={styles.clap}
onClick={handleClick}
{...restProps}
>
{children}
</button>
)
}
const ClapIcon = ({ isClicked }) => {
return (
<span>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='-549 338 100.1 125'
className={`${styles.icon} ${isClicked && styles.checked}`}
>
<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' />
<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' />
</svg>
</span>
)
}
const ClapCount = ({ count, setRef, ...restProps }) => {
return (
<span ref={setRef} className={styles.count} {...restProps}>
+ {count}
</span>
)
}
const CountTotal = ({ countTotal, setRef, ...restProps }) => {
return (
<span ref={setRef} className={styles.total} {...restProps}>
{countTotal}
</span>
)
}
/**
* Usage
*/
const userInitialState = {
count: 0,
countTotal: 1000,
isClicked: false
}
const Usage = () => {
const {
clapState,
getTogglerProps,
getCounterProps,
reset,
resetDep
} = useClapState(userInitialState)
const { count, countTotal, isClicked } = clapState
const [{ clapRef, clapCountRef, clapTotalRef }, setRef] = useDOMRef()
const animationTimeline = useClapAnimation({
clapEl: clapRef,
countEl: clapCountRef,
clapTotalEl: clapTotalRef
})
useEffectAfterMount(() => {
animationTimeline.replay()
}, [count])
const [uploadingReset, setUpload] = useState(false)
useEffectAfterMount(() => {
setUpload(true)
const id = setTimeout(() => {
setUpload(false)
}, 3000)
return () => clearTimeout(id)
}, [resetDep])
const handleClick = () => {
console.log('CLICKED!!!!')
}
return (
<div>
<ClapContainer
setRef={setRef}
data-refkey='clapRef'
{...getTogglerProps({
onClick: handleClick,
'aria-pressed': false
})}
>
<ClapIcon isClicked={isClicked} />
<ClapCount
setRef={setRef}
data-refkey='clapCountRef'
{...getCounterProps()}
/>
<CountTotal
countTotal={countTotal}
setRef={setRef}
data-refkey='clapTotalRef'
/>
</ClapContainer>
<section>
<button onClick={reset} className={userStyles.resetBtn}>
reset
</button>
<pre className={userStyles.resetMsg}>
{JSON.stringify({ count, countTotal, isClicked })}
</pre>
<pre className={userStyles.resetMsg}>
{uploadingReset ? `uploading reset ${resetDep} ...` : ''}
</pre>
</section>
</div>
)
}
export default Usage
import React, {
useState,
useLayoutEffect,
useCallback,
useRef,
useEffect,
useReducer
} from 'react'
import mojs from 'mo-js'
import styles from './index.css'
import userStyles from './usage.css'
const INITIAL_STATE = {
count: 0,
countTotal: 267,
isClicked: false
}
/**
* Custom Hook for animation
*/
const useClapAnimation = ({ clapEl, countEl, clapTotalEl }) => {
const [animationTimeline, setAnimationTimeline] = useState(
() => new mojs.Timeline()
)
useLayoutEffect(() => {
if (!clapEl || !countEl || !clapTotalEl) {
return
}
const tlDuration = 300
const scaleButton = new mojs.Html({
el: clapEl,
duration: tlDuration,
scale: { 1.3: 1 },
easing: mojs.easing.ease.out
})
const triangleBurst = new mojs.Burst({
parent: clapEl,
radius: { 50: 95 },
count: 5,
angle: 30,
children: {
shape: 'polygon',
radius: { 6: 0 },
stroke: 'rgba(211,54,0,0.5)',
strokeWidth: 2,
angle: 210,
delay: 30,
speed: 0.2,
easing: mojs.easing.bezier(0.1, 1, 0.3, 1),
duration: tlDuration
}
})
const circleBurst = new mojs.Burst({
parent: clapEl,
radius: { 50: 75 },
angle: 25,
duration: tlDuration,
children: {
shape: 'circle',
fill: 'rgba(149,165,166,0.5)',
delay: 30,
speed: 0.2,
radius: { 3: 0 },
easing: mojs.easing.bezier(0.1, 1, 0.3, 1)
}
})
const countAnimation = new mojs.Html({
el: countEl,
opacity: { 0: 1 },
y: { 0: -30 },
duration: tlDuration
}).then({
opacity: { 1: 0 },
y: -80,
delay: tlDuration / 2
})
const countTotalAnimation = new mojs.Html({
el: clapTotalEl,
opacity: { 0: 1 },
delay: (3 * tlDuration) / 2,
duration: tlDuration,
y: { 0: -3 }
})
if (typeof clapEl === 'string') {
const clap = document.getElementById('clap')
clap.style.transform = 'scale(1,1)'
} else {
clapEl.style.transform = 'scale(1,1)'
}
const newAnimationTimeline = animationTimeline.add([
scaleButton,
countTotalAnimation,
countAnimation,
triangleBurst,
circleBurst
])
setAnimationTimeline(newAnimationTimeline)
}, [clapEl, countEl, clapTotalEl])
return animationTimeline
}
/**
* useDOMRef Hook
*/
const useDOMRef = () => {
const [DOMRef, setRefState] = useState({})
const setRef = useCallback(node => {
setRefState(prevRefState => ({
...prevRefState,
[node.dataset.refkey]: node
}))
}, [])
return [DOMRef, setRef]
}
/**
*
* custom hook for getting preivous prop/state
*/
const usePrevious = value => {
const ref = useRef()
useEffect(() => {
ref.current = value
})
return ref.current
}
// const handleClick = (evt) => { ... }
// <button onClick={handleClick} />
const callFnsInSequence = (...fns) => (...args) => {
fns.forEach(fn => fn && fn(...args))
}
/**
* custom hook for useClapState
*/
const MAXIMUM_USER_CLAP = 50
const internalReducer = ({ count, countTotal }, { type, payload }) => {
switch (type) {
case 'clap':
return {
isClicked: true,
count: Math.min(count + 1, MAXIMUM_USER_CLAP),
countTotal: count < MAXIMUM_USER_CLAP ? countTotal + 1 : countTotal
}
case 'reset':
return payload
default:
break
}
}
const useClapState = (
initialState = INITIAL_STATE,
reducer = internalReducer
) => {
const userInitialState = useRef(initialState)
const [clapState, dispatch] = useReducer(reducer, initialState)
const { count, countTotal } = clapState
const updateClapState = () => dispatch({ type: 'clap' })
// glorified counter
const resetRef = useRef(0)
const prevCount = usePrevious(count)
const reset = useCallback(() => {
// ⚠️ The video lesson had this wrapped in an if statement which I've removed ...
// owing to the bug opened by Matija here https://www.udemy.com/instructor/communication/qa/9651560/detail/
dispatch({ type: 'reset', payload: userInitialState.current })
resetRef.current++
}, [prevCount, count, dispatch])
const getTogglerProps = ({ onClick, ...otherProps } = {}) => ({
onClick: callFnsInSequence(updateClapState, onClick),
'aria-pressed': clapState.isClicked,
...otherProps
})
const getCounterProps = ({ ...otherProps }) => ({
count,
'aria-valuemax': MAXIMUM_USER_CLAP,
'aria-valuemin': 0,
'aria-valuenow': count,
...otherProps
})
return {
clapState,
updateClapState,
getTogglerProps,
getCounterProps,
reset,
resetDep: resetRef.current
}
}
useClapState.reducer = internalReducer
useClapState.types = {
clap: 'clap',
reset: 'reset'
}
/**
* custom useEffectAfterMount hook
*/
const useEffectAfterMount = (cb, deps) => {
const componentJustMounted = useRef(true)
useEffect(() => {
if (!componentJustMounted.current) {
return cb()
}
componentJustMounted.current = false
}, deps)
}
/**
* subcomponents
*/
const ClapContainer = ({ children, setRef, handleClick, ...restProps }) => {
return (
<button
ref={setRef}
className={styles.clap}
onClick={handleClick}
{...restProps}
>
{children}
</button>
)
}
const ClapIcon = ({ isClicked }) => {
return (
<span>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='-549 338 100.1 125'
className={`${styles.icon} ${isClicked && styles.checked}`}
>
<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' />
<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' />
</svg>
</span>
)
}
const ClapCount = ({ count, setRef, ...restProps }) => {
return (
<span ref={setRef} className={styles.count} {...restProps}>
+ {count}
</span>
)
}
const CountTotal = ({ countTotal, setRef, ...restProps }) => {
return (
<span ref={setRef} className={styles.total} {...restProps}>
{countTotal}
</span>
)
}
/**
* Usage
*/
const userInitialState = {
count: 0,
countTotal: 1000,
isClicked: false
}
const Usage = () => {
const [timesClapped, setTimeClapped] = useState(0)
const isClappedTooMuch = timesClapped >= 7 // true/false
const reducer = (state, action) => {
if (action.type === useClapState.types.clap && isClappedTooMuch) {
return state
}
return useClapState.reducer(state, action)
}
const {
clapState,
getTogglerProps,
getCounterProps,
reset,
resetDep
} = useClapState(userInitialState, reducer)
const { count, countTotal, isClicked } = clapState
const [{ clapRef, clapCountRef, clapTotalRef }, setRef] = useDOMRef()
const animationTimeline = useClapAnimation({
clapEl: clapRef,
countEl: clapCountRef,
clapTotalEl: clapTotalRef
})
useEffectAfterMount(() => {
animationTimeline.replay()
}, [count])
const [uploadingReset, setUpload] = useState(false)
useEffectAfterMount(() => {
setUpload(true)
setTimeClapped(0)
const id = setTimeout(() => {
setUpload(false)
}, 3000)
return () => clearTimeout(id)
}, [resetDep])
const handleClick = () => {
setTimeClapped(t => t + 1)
}
return (
<div>
<ClapContainer
setRef={setRef}
data-refkey='clapRef'
{...getTogglerProps({
onClick: handleClick,
'aria-pressed': false
})}
>
<ClapIcon isClicked={isClicked} />
<ClapCount
setRef={setRef}
data-refkey='clapCountRef'
{...getCounterProps()}
/>
<CountTotal
countTotal={countTotal}
setRef={setRef}
data-refkey='clapTotalRef'
/>
</ClapContainer>
<section>
<button onClick={reset} className={userStyles.resetBtn}>
reset
</button>
<pre className={userStyles.resetMsg}>
{JSON.stringify({ timesClapped, count, countTotal })}
</pre>
<pre className={userStyles.resetMsg}>
{uploadingReset ? `uploading reset ${resetDep} ...` : ''}
</pre>
<pre style={{ color: 'red' }}>
{isClappedTooMuch
? `You have clapped too much. Don't be so generous!`
: ''}
</pre>
</section>
</div>
)
}
export default Usage
En el caso anterior, se ha tenido que replicar casi todo el reducer para conseguir un comportamiento parecido al que tiene por defecto.
Lo mejor sería poder exportar el reducer y los types por defecto.
En primer lugar, exportamos el reducer y types por defecto en useClapState
:
useClapState.reducer = internalReducer
useClapState.types = {
clap: 'clap',
reset: 'reset'
}
Después, en el uso de este componente, se puede sobreescribir el reducer o extender su funcionalidad:
const reducer = (state, action) => {
if (action.type === useClapState.types.clap && isClappedTooMuch) {
return state
}
return useClapState.reducer(state, action)
}
import React, {
useState,
useLayoutEffect,
useCallback,
useRef,
useEffect,
useReducer
} from 'react'
import mojs from 'mo-js'
import styles from './index.css'
import userStyles from './usage.css'
const INITIAL_STATE = {
count: 0,
countTotal: 267,
isClicked: false
}
/**
* Custom Hook for animation
*/
const useClapAnimation = ({ clapEl, countEl, clapTotalEl }) => {
const [animationTimeline, setAnimationTimeline] = useState(
() => new mojs.Timeline()
)
useLayoutEffect(() => {
if (!clapEl || !countEl || !clapTotalEl) {
return
}
const tlDuration = 300
const scaleButton = new mojs.Html({
el: clapEl,
duration: tlDuration,
scale: { 1.3: 1 },
easing: mojs.easing.ease.out
})
const triangleBurst = new mojs.Burst({
parent: clapEl,
radius: { 50: 95 },
count: 5,
angle: 30,
children: {
shape: 'polygon',
radius: { 6: 0 },
stroke: 'rgba(211,54,0,0.5)',
strokeWidth: 2,
angle: 210,
delay: 30,
speed: 0.2,
easing: mojs.easing.bezier(0.1, 1, 0.3, 1),
duration: tlDuration
}
})
const circleBurst = new mojs.Burst({
parent: clapEl,
radius: { 50: 75 },
angle: 25,
duration: tlDuration,
children: {
shape: 'circle',
fill: 'rgba(149,165,166,0.5)',
delay: 30,
speed: 0.2,
radius: { 3: 0 },
easing: mojs.easing.bezier(0.1, 1, 0.3, 1)
}
})
const countAnimation = new mojs.Html({
el: countEl,
opacity: { 0: 1 },
y: { 0: -30 },
duration: tlDuration
}).then({
opacity: { 1: 0 },
y: -80,
delay: tlDuration / 2
})
const countTotalAnimation = new mojs.Html({
el: clapTotalEl,
opacity: { 0: 1 },
delay: (3 * tlDuration) / 2,
duration: tlDuration,
y: { 0: -3 }
})
if (typeof clapEl === 'string') {
const clap = document.getElementById('clap')
clap.style.transform = 'scale(1,1)'
} else {
clapEl.style.transform = 'scale(1,1)'
}
const newAnimationTimeline = animationTimeline.add([
scaleButton,
countTotalAnimation,
countAnimation,
triangleBurst,
circleBurst
])
setAnimationTimeline(newAnimationTimeline)
}, [clapEl, countEl, clapTotalEl])
return animationTimeline
}
/**
* useDOMRef Hook
*/
const useDOMRef = () => {
const [DOMRef, setRefState] = useState({})
const setRef = useCallback(node => {
setRefState(prevRefState => ({
...prevRefState,
[node.dataset.refkey]: node
}))
}, [])
return [DOMRef, setRef]
}
/**
*
* custom hook for getting preivous prop/state
*/
const usePrevious = value => {
const ref = useRef()
useEffect(() => {
ref.current = value
})
return ref.current
}
// const handleClick = (evt) => { ... }
// <button onClick={handleClick} />
const callFnsInSequence = (...fns) => (...args) => {
fns.forEach(fn => fn && fn(...args))
}
/**
* custom hook for useClapState
*/
const MAXIMUM_USER_CLAP = 50
const internalReducer = ({ count, countTotal }, { type, payload }) => {
switch (type) {
case 'clap':
return {
isClicked: true,
count: Math.min(count + 1, MAXIMUM_USER_CLAP),
countTotal: count < MAXIMUM_USER_CLAP ? countTotal + 1 : countTotal
}
case 'reset':
return payload
default:
break
}
}
const useClapState = (
initialState = INITIAL_STATE,
reducer = internalReducer
) => {
const userInitialState = useRef(initialState)
const [clapState, dispatch] = useReducer(reducer, initialState)
const { count, countTotal } = clapState
const updateClapState = () => dispatch({ type: 'clap' })
// glorified counter
const resetRef = useRef(0)
const prevCount = usePrevious(count)
const reset = useCallback(() => {
// ⚠️ The video lesson had this wrapped in an if statement which I've removed ...
// owing to the bug opened by Matija here https://www.udemy.com/instructor/communication/qa/9651560/detail/
dispatch({ type: 'reset', payload: userInitialState.current })
resetRef.current++
}, [prevCount, count, dispatch])
const getTogglerProps = ({ onClick, ...otherProps } = {}) => ({
onClick: callFnsInSequence(updateClapState, onClick),
'aria-pressed': clapState.isClicked,
...otherProps
})
const getCounterProps = ({ ...otherProps }) => ({
count,
'aria-valuemax': MAXIMUM_USER_CLAP,
'aria-valuemin': 0,
'aria-valuenow': count,
...otherProps
})
return {
clapState,
updateClapState,
getTogglerProps,
getCounterProps,
reset,
resetDep: resetRef.current
}
}
useClapState.reducer = internalReducer
useClapState.types = {
clap: 'clap',
reset: 'reset'
}
/**
* custom useEffectAfterMount hook
*/
const useEffectAfterMount = (cb, deps) => {
const componentJustMounted = useRef(true)
useEffect(() => {
if (!componentJustMounted.current) {
return cb()
}
componentJustMounted.current = false
}, deps)
}
/**
* subcomponents
*/
const ClapContainer = ({ children, setRef, handleClick, ...restProps }) => {
return (
<button
ref={setRef}
className={styles.clap}
onClick={handleClick}
{...restProps}
>
{children}
</button>
)
}
const ClapIcon = ({ isClicked }) => {
return (
<span>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='-549 338 100.1 125'
className={`${styles.icon} ${isClicked && styles.checked}`}
>
<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' />
<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' />
</svg>
</span>
)
}
const ClapCount = ({ count, setRef, ...restProps }) => {
return (
<span ref={setRef} className={styles.count} {...restProps}>
+ {count}
</span>
)
}
const CountTotal = ({ countTotal, setRef, ...restProps }) => {
return (
<span ref={setRef} className={styles.total} {...restProps}>
{countTotal}
</span>
)
}
/**
* Usage
*/
const userInitialState = {
count: 0,
countTotal: 1000,
isClicked: false
}
const Usage = () => {
const [timesClapped, setTimeClapped] = useState(0)
const isClappedTooMuch = timesClapped >= 7 // true/false
const reducer = (state, action) => {
if (action.type === useClapState.types.clap && isClappedTooMuch) {
return state
}
return useClapState.reducer(state, action)
}
const {
clapState,
getTogglerProps,
getCounterProps,
reset,
resetDep
} = useClapState(userInitialState, reducer)
const { count, countTotal, isClicked } = clapState
const [{ clapRef, clapCountRef, clapTotalRef }, setRef] = useDOMRef()
const animationTimeline = useClapAnimation({
clapEl: clapRef,
countEl: clapCountRef,
clapTotalEl: clapTotalRef
})
useEffectAfterMount(() => {
animationTimeline.replay()
}, [count])
const [uploadingReset, setUpload] = useState(false)
useEffectAfterMount(() => {
setUpload(true)
setTimeClapped(0)
const id = setTimeout(() => {
setUpload(false)
}, 3000)
return () => clearTimeout(id)
}, [resetDep])
const handleClick = () => {
setTimeClapped(t => t + 1)
}
return (
<div>
<ClapContainer
setRef={setRef}
data-refkey='clapRef'
{...getTogglerProps({
onClick: handleClick,
'aria-pressed': false
})}
>
<ClapIcon isClicked={isClicked} />
<ClapCount
setRef={setRef}
data-refkey='clapCountRef'
{...getCounterProps()}
/>
<CountTotal
countTotal={countTotal}
setRef={setRef}
data-refkey='clapTotalRef'
/>
</ClapContainer>
<section>
<button onClick={reset} className={userStyles.resetBtn}>
reset
</button>
<pre className={userStyles.resetMsg}>
{JSON.stringify({ timesClapped, count, countTotal })}
</pre>
<pre className={userStyles.resetMsg}>
{uploadingReset ? `uploading reset ${resetDep} ...` : ''}
</pre>
<pre style={{ color: 'red' }}>
{isClappedTooMuch
? `You have clapped too much. Don't be so generous!`
: ''}
</pre>
</section>
</div>
)
}
export default Usage