/
githubmirror
/
preact
Обзор
Документация
Войти
/
githubmirror
/
preact
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
hooks/test/browser/useState.test.jsx
578 строк
12 KB
Jovi De Croock
Commit pending hook state in options._render
31 июл 2026, 10:09
31 июл 2026, 10:09
d18a874
Код
Авторство
О чём код?
import { setupRerender, act } from 'preact/test-utils'; import { createElement, render, createContext, Component } from 'preact'; import { vi } from 'vitest'; import { useState, useContext, useEffect } from 'preact/hooks'; import { setupScratch, teardown } from '../../../test/_util/helpers'; describe('useState', () => { /** @type {HTMLDivElement} */ let scratch; /** @type {() => void} */ let rerender; beforeEach(() => { scratch = setupScratch(); rerender = setupRerender(); }); afterEach(() => { Component.prototype.shouldComponentUpdate = undefined; teardown(scratch); }); it('serves the same state across render calls', () => { const stateHistory = []; function Comp() { const [state] = useState({ a: 1 }); stateHistory.push(state); return null; } render(<Comp />, scratch); render(<Comp />, scratch); expect(stateHistory).to.deep.equal([{ a: 1 }, { a: 1 }]); expect(stateHistory[0]).to.equal(stateHistory[1]); }); it('can initialize the state via a function', () => { const initState = vi.fn(() => 1); function Comp() { useState(initState); return null; } render(<Comp />, scratch); render(<Comp />, scratch); expect(initState).toHaveBeenCalledOnce(); }); it('does not rerender on equal state', () => { let lastState; let doSetState; const Comp = vi.fn(() => { const [state, setState] = useState(0); lastState = state; doSetState = setState; return null; }); render(<Comp />, scratch); expect(lastState).to.equal(0); expect(Comp).toHaveBeenCalledOnce(); doSetState(0); rerender(); expect(lastState).to.equal(0); expect(Comp).toHaveBeenCalledOnce(); doSetState(() => 0); rerender(); expect(lastState).to.equal(0); expect(Comp).toHaveBeenCalledOnce(); }); it('rerenders when setting the state', () => { let lastState; let doSetState; const Comp = vi.fn(() => { const [state, setState] = useState(0); lastState = state; doSetState = setState; return null; }); render(<Comp />, scratch); expect(lastState).to.equal(0); expect(Comp).toHaveBeenCalledOnce(); doSetState(1); rerender(); expect(lastState).to.equal(1); expect(Comp).toHaveBeenCalledTimes(2); // Updater function style doSetState(current => current * 10); rerender(); expect(lastState).to.equal(10); expect(Comp).toHaveBeenCalledTimes(3); }); it('can be set by another component', () => { function StateContainer() { const [count, setCount] = useState(0); return ( <div> <p>Count: {count}</p> <Increment increment={() => setCount(c => c + 10)} /> </div> ); } function Increment(props) { return <button onClick={props.increment}>Increment</button>; } render(<StateContainer />, scratch); expect(scratch.textContent).to.include('Count: 0'); const button = scratch.querySelector('button'); button.click(); rerender(); expect(scratch.textContent).to.include('Count: 10'); }); it('should correctly initialize', () => { let scopedThing = 'hi'; let arg; function useSomething() { const args = useState(setup); function setup(thing = scopedThing) { arg = thing; return thing; } return args; } const App = () => { const [state] = useSomething(); return <p>{state}</p>; }; render(<App />, scratch); expect(arg).to.equal('hi'); expect(scratch.innerHTML).to.equal('<p>hi</p>'); }); it('should correctly re-initialize when first run threw an error', () => { let hasThrown = false; let setup = vi.fn(() => { if (!hasThrown) { hasThrown = true; throw new Error('test'); } else { return 'hi'; } }); const App = () => { const state = useState(setup)[0]; return <p>{state}</p>; }; expect(() => render(<App />, scratch)).to.throw('test'); expect(setup).toHaveBeenCalledOnce(); expect(() => render(<App />, scratch)).not.to.throw(); expect(setup).toHaveBeenCalledTimes(2); expect(scratch.innerHTML).to.equal('<p>hi</p>'); }); it('should handle queued useState', () => { function Message({ message, onClose }) { const [isVisible, setVisible] = useState(Boolean(message)); const [prevMessage, setPrevMessage] = useState(message); if (message !== prevMessage) { setPrevMessage(message); setVisible(Boolean(message)); } if (!isVisible) { return null; } return <p onClick={onClose}>{message}</p>; } function App() { const [message, setMessage] = useState('Click Here!!'); return ( <Message onClose={() => { setMessage(''); }} message={message} /> ); } render(<App />, scratch); expect(scratch.textContent).to.equal('Click Here!!'); const text = scratch.querySelector('p'); text.click(); rerender(); expect(scratch.innerHTML).to.equal(''); }); it('should render a second time when the render function updates state', () => { const calls = []; const App = () => { const [greeting, setGreeting] = useState('bye'); if (greeting === 'bye') { setGreeting('hi'); } calls.push(greeting); return <p>{greeting}</p>; }; act(() => { render(<App />, scratch); }); expect(calls.length).to.equal(2); expect(calls).to.deep.equal(['bye', 'hi']); expect(scratch.textContent).to.equal('hi'); }); // https://github.com/preactjs/preact/issues/3669 it('correctly updates with multiple state updates', () => { let simulateClick; function TestWidget() { const [saved, setSaved] = useState(false); const [, setSaving] = useState(false); simulateClick = () => { setSaving(true); setSaved(true); setSaving(false); }; return <div>{saved ? 'Saved!' : 'Unsaved!'}</div>; } render(<TestWidget />, scratch); expect(scratch.innerHTML).to.equal('<div>Unsaved!</div>'); act(() => { simulateClick(); }); expect(scratch.innerHTML).to.equal('<div>Saved!</div>'); }); // https://github.com/preactjs/preact/issues/3674 it('ensure we iterate over all hooks', () => { let open, close; function TestWidget() { const [, setCounter] = useState(0); const [isOpen, setOpen] = useState(false); open = () => { setCounter(42); setOpen(true); }; close = () => { setOpen(false); }; return <div>{isOpen ? 'open' : 'closed'}</div>; } render(<TestWidget />, scratch); expect(scratch.innerHTML).to.equal('<div>closed</div>'); act(() => { open(); }); expect(scratch.innerHTML).to.equal('<div>open</div>'); act(() => { close(); }); expect(scratch.innerHTML).to.equal('<div>closed</div>'); }); it('does not loop when states are equal after batches', () => { const renderSpy = vi.fn(); const Context = createContext(null); function ModalProvider(props) { let [modalCount, setModalCount] = useState(0); renderSpy(modalCount); let context = { modalCount, addModal() { setModalCount(count => count + 1); }, removeModal() { setModalCount(count => count - 1); } }; return ( <Context.Provider value={context}>{props.children}</Context.Provider> ); } function useModal() { let context = useContext(Context); useEffect(() => { context.addModal(); return () => { context.removeModal(); }; }, [context]); } function Popover() { useModal(); return <div>Popover</div>; } function App() { return ( <ModalProvider> <Popover /> </ModalProvider> ); } act(() => { render(<App />, scratch); }); expect(renderSpy).toHaveBeenCalledTimes(2); }); it('Cancels effect invocations correctly when bailing', () => { const renderSpy = vi.fn(); const cleanupSpy = vi.fn(); const spy = vi.fn(); let set; function App() { const [state, setState] = useState('initial'); set = setState; renderSpy(); useEffect(() => { spy(); return () => { cleanupSpy(); }; }); return <p>{state}</p>; } act(() => { render(<App />, scratch); }); expect(renderSpy).toHaveBeenCalledOnce(); expect(spy).toHaveBeenCalledOnce(); expect(cleanupSpy).not.toHaveBeenCalled(); act(() => { set('updated'); set('initial'); }); expect(renderSpy).toHaveBeenCalledTimes(1); expect(spy).toHaveBeenCalledOnce(); expect(cleanupSpy).not.toHaveBeenCalled(); }); // see preactjs/preact#3731 it('respects updates initiated from the parent', () => { let setChild, setParent; const Child = props => { const [, setState] = useState(false); setChild = setState; return <p>{props.text}</p>; }; const Parent = () => { const [state, setState] = useState('hello world'); setParent = setState; return <Child text={state} />; }; render(<Parent />, scratch); expect(scratch.innerHTML).to.equal('<p>hello world</p>'); setParent('hello world!!!'); setChild(true); setChild(false); rerender(); expect(scratch.innerHTML).to.equal('<p>hello world!!!</p>'); }); it('should limit rerenders when setting state to NaN', () => { const calls = []; const App = ({ i }) => { calls.push('rendering' + i); const [greeting, setGreeting] = useState(0); if (i === 2) { setGreeting(NaN); } return <p>{greeting}</p>; }; act(() => { render(<App i={1} />, scratch); }); expect(calls.length).to.equal(1); expect(calls).to.deep.equal(['rendering1']); act(() => { render(<App i={2} />, scratch); }); expect(calls.length).to.equal(3); expect(calls.slice(1).every(c => c === 'rendering2')).to.equal(true); }); describe('Global sCU', () => { let prevScu; beforeAll(() => { prevScu = Component.prototype.shouldComponentUpdate; Component.prototype.shouldComponentUpdate = () => { return true; }; }); afterAll(() => { Component.prototype.shouldComponentUpdate = prevScu; }); it('correctly updates with multiple state updates', () => { let simulateClick; let renders = 0; function TestWidget() { renders++; const [saved, setSaved] = useState(false); simulateClick = () => { setSaved(true); setSaved(false); }; return <div>{saved ? 'Saved!' : 'Unsaved!'}</div>; } render(<TestWidget />, scratch); expect(scratch.innerHTML).to.equal('<div>Unsaved!</div>'); expect(renders).to.equal(1); act(() => { simulateClick(); }); expect(scratch.innerHTML).to.equal('<div>Unsaved!</div>'); expect(renders).to.equal(2); }); }); it('Works when we combine strict equality, signals bail and state settling', () => { // In signals we bail when we are using no signals/computeds/.... Component.prototype.shouldComponentUpdate = function ( nextProps, nextState ) { return false; }; let setA, setB; const fooContext = createContext(); const barContext = createContext(); function FooProvider({ children }) { const [a, _setA] = useState(0); setA = _setA; return <fooContext.Provider value={a}>{children}</fooContext.Provider>; } function BarProvider({ children }) { const [b, _setB] = useState(0); setB = _setB; return <barContext.Provider value={b}>{children}</barContext.Provider>; } function Child() { const a = useContext(fooContext); const b = useContext(barContext); return ( <p> {a}-{b} </p> ); } function App() { return ( <FooProvider> <BarProvider> <Child /> </BarProvider> </FooProvider> ); } render(<App />, scratch); expect(scratch.innerHTML).to.equal('<p>0-0</p>'); act(() => { // We update A first so that we have a top-down render going on setA(1); // The update will bail at B, we don't want sCU to run because // else we risk the state's _nextValue being settled too early // and thus applying the update too early and bailing on the subsequent // render due to the values already being applied. setB(1); }); expect(scratch.innerHTML).to.equal('<p>1-1</p>'); }); it('applies pending state on a forced update', () => { const ctx = createContext(0); let setValue, setCount; function Provider({ children }) { const [value, _setValue] = useState(0); setValue = _setValue; return <ctx.Provider value={value}>{children}</ctx.Provider>; } function Child() { const value = useContext(ctx); const [count, _setCount] = useState(0); setCount = _setCount; return ( <p> {value}-{count} </p> ); } render( <Provider> <Child /> </Provider>, scratch ); expect(scratch.innerHTML).to.equal('<p>0-0</p>'); act(() => { // A context change force-updates its subscribers, so Child renders // with sCU skipped entirely. setValue(1); setCount(1); }); expect(scratch.innerHTML).to.equal('<p>1-1</p>'); }); });