set-state-compare
Version:
setState for React that compares with the current state and only sets the state if changed.
227 lines (177 loc) • 5.93 kB
JavaScript
import {anythingDifferent} from "./diff-utils.js"
import {dig} from "diggerize"
import fetchingObject from "fetching-object"
import memoCompareProps from "./memo-compare-props.js"
import PropTypes from "prop-types"
import shared from "./shared.js"
import {useEffect, useMemo, useState} from "react"
class ShapeComponent {
constructor(props) {
this.__caches = {}
this.__mounting = true
this.__mounted = false
this.props = props
this.setStates = {}
this.state = {}
this.__firstRenderCompleted = false
this.tt = fetchingObject(this)
this.p = fetchingObject(() => this.props)
this.s = fetchingObject(this.state)
}
cache(name, value, dependencies) {
const oldDependencies = this.__caches[name]?.dependencies
if (typeof value == "function") {
value = value()
}
if (!(name in this.__caches) || anythingDifferent(oldDependencies, dependencies)) {
this.__caches[name] = {dependencies, value}
}
return this.__caches[name].value
}
setInstance(variables) {
for (const name in variables) {
this[name] = variables[name]
}
}
setState(statesList, callback) {
if (typeof statesList == "function") {
statesList = statesList(this.state)
}
for (const stateName in statesList) {
const newValue = statesList[stateName]
if (!(stateName in this.setStates)) {
throw new Error(`No such state: ${stateName}`)
}
this.setStates[stateName](newValue)
}
if (callback) {
shared.renderingCallbacks.push(callback)
}
}
stylingFor(stylingName, style = {}) {
let customStyling = dig(this, "props", "styles", stylingName)
if (typeof customStyling == "function") {
customStyling = customStyling({state: this.state, style})
}
if (customStyling) {
return Object.assign(style, customStyling)
}
return style
}
useState(stateName, defaultValue) {
const [stateValue, setState] = useState(defaultValue)
if (!(stateName in this.state)) {
this.state[stateName] = stateValue
this.setStates[stateName] = (newValue, args) => {
if (anythingDifferent(this.state[stateName], newValue)) {
let prevState
if (this.componentDidUpdate) {
prevState = Object.assign({}, this.state)
}
this.state[stateName] = newValue
// Avoid React error if using set-state while rendering (like in a useMemo callback)
if (!args?.silent) {
if (shared.rendering > 0) {
shared.renderingCallbacks.push(() => {
setState(newValue)
})
} else {
setState(newValue)
}
}
if (this.componentDidUpdate) {
this.componentDidUpdate(this.props, prevState)
}
}
}
}
return this.setStates[stateName]
}
useStates(statesList) {
if (Array.isArray(statesList)) {
for(const stateName of statesList) {
this.useState(stateName)
}
} else {
for(const stateName in statesList) {
const defaultValue = statesList[stateName]
this.useState(stateName, defaultValue)
}
}
}
}
const shapeComponent = (ShapeClass) => {
const functionalComponent = (props) => {
// Count rendering to avoid setting state while rendering which causes a console-error from React
shared.rendering += 1
try {
// Calculate and validate props
let actualProps
if (ShapeClass.defaultProps) {
// Undefined values are removed from the props because they shouldn't override default values
const propsWithoutUndefined = Object.assign({}, props)
for (const key in propsWithoutUndefined) {
const value = propsWithoutUndefined[key]
if (value === undefined) {
delete propsWithoutUndefined[key]
}
}
actualProps = Object.assign({}, ShapeClass.defaultProps, propsWithoutUndefined)
} else {
actualProps = props
}
if (ShapeClass.propTypes) {
const validateProps = {}
for (const key in actualProps) {
// Accessing 'key' will result in a warning in the console
if (key == "key") continue
validateProps[key] = actualProps[key]
}
PropTypes.checkPropTypes(ShapeClass.propTypes, validateProps, "prop", ShapeClass.name)
}
const shape = useMemo(() => new ShapeClass(actualProps), [])
const prevProps = shape.props
shape.props = actualProps
if (shape.setup) {
shape.setup()
}
if (shape.componentDidUpdate && shape.__firstRenderCompleted && memoCompareProps(shape.props, props)) {
shape.componentDidUpdate(prevProps, shape.state)
}
useEffect(() => {
shape.__mounting = false
shape.__mounted = true
if (shape.componentDidMount) {
shape.componentDidMount()
}
return () => {
shape.__mounted = false
if (shape.componentWillUnmount) {
shape.componentWillUnmount()
}
}
}, [])
shape.__firstRenderCompleted = true
// Run any callbacks added by setState(states, callback) once rendering is done
useEffect(() => {
if (shared.rendering == 0) {
try {
for (const callback of shared.renderingCallbacks) {
new Promise(() => callback())
}
} finally {
shared.renderingCallbacks.length = 0
}
}
})
// Finally render the component and return it
return shape.render()
} finally {
shared.rendering -= 1
}
}
functionalComponent.displayName = ShapeClass.name
Object.defineProperty(functionalComponent, "name", {value: ShapeClass.name})
return functionalComponent
}
export {shapeComponent, ShapeComponent}