Repository: preactjs/preact Branch: main Commit: 21dd6d04c1a9 Files: 295 Total size: 1.3 MB Directory structure: gitextract_o9kdplb8/ ├── .editorconfig ├── .github/ │ ├── CODE_OF_CONDUCT.md │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ └── workflows/ │ ├── benchmarks.yml │ ├── build-test.yml │ ├── ci.yml │ ├── pr-reporter.yml │ ├── release.yml │ ├── run-bench.yml │ ├── single-bench.yml │ └── size.yml ├── .gitignore ├── .gitmodules ├── .husky/ │ └── pre-commit ├── .oxlintrc.json ├── .prettierignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── babel.config.js ├── biome.json ├── compat/ │ ├── LICENSE │ ├── client.d.ts │ ├── client.js │ ├── client.mjs │ ├── jsx-dev-runtime.js │ ├── jsx-dev-runtime.mjs │ ├── jsx-runtime.js │ ├── jsx-runtime.mjs │ ├── mangle.json │ ├── package.json │ ├── scheduler.d.ts │ ├── scheduler.js │ ├── scheduler.mjs │ ├── server.browser.js │ ├── server.d.ts │ ├── server.js │ ├── server.mjs │ ├── src/ │ │ ├── Children.js │ │ ├── PureComponent.js │ │ ├── forwardRef.js │ │ ├── hooks.js │ │ ├── index.d.ts │ │ ├── index.js │ │ ├── internal.d.ts │ │ ├── memo.js │ │ ├── portals.js │ │ ├── render.js │ │ ├── suspense.d.ts │ │ ├── suspense.js │ │ └── util.js │ ├── test/ │ │ ├── browser/ │ │ │ ├── Children.test.jsx │ │ │ ├── PureComponent.test.jsx │ │ │ ├── cloneElement.test.jsx │ │ │ ├── compat.options.test.jsx │ │ │ ├── component.test.jsx │ │ │ ├── componentDidCatch.test.jsx │ │ │ ├── context.test.jsx │ │ │ ├── createElement.test.jsx │ │ │ ├── createFactory.test.jsx │ │ │ ├── events.test.jsx │ │ │ ├── exports.test.js │ │ │ ├── findDOMNode.test.jsx │ │ │ ├── forwardRef.test.jsx │ │ │ ├── hooks.test.jsx │ │ │ ├── hydrate.test.jsx │ │ │ ├── isFragment.test.js │ │ │ ├── isMemo.test.jsx │ │ │ ├── isValidElement.test.js │ │ │ ├── memo.test.jsx │ │ │ ├── portals.test.jsx │ │ │ ├── render.test.jsx │ │ │ ├── scheduler.test.js │ │ │ ├── select.test.jsx │ │ │ ├── suspense-hydration.test.jsx │ │ │ ├── suspense-utils.js │ │ │ ├── suspense.test.jsx │ │ │ ├── svg.test.jsx │ │ │ ├── testUtils.js │ │ │ ├── textarea.test.jsx │ │ │ ├── unmountComponentAtNode.test.jsx │ │ │ ├── unstable_batchedUpdates.test.js │ │ │ └── useSyncExternalStore.test.jsx │ │ └── ts/ │ │ ├── forward-ref.tsx │ │ ├── index.tsx │ │ ├── lazy.tsx │ │ ├── memo.tsx │ │ ├── react-default.tsx │ │ ├── react-star.tsx │ │ ├── scheduler.ts │ │ ├── suspense.tsx │ │ ├── tsconfig.json │ │ └── utils.ts │ ├── test-utils.js │ └── test-utils.mjs ├── config/ │ └── compat-entries.js ├── debug/ │ ├── LICENSE │ ├── mangle.json │ ├── package.json │ ├── src/ │ │ ├── check-props.js │ │ ├── component-stack.js │ │ ├── constants.js │ │ ├── debug.js │ │ ├── index.d.ts │ │ ├── index.js │ │ ├── internal.d.ts │ │ └── util.js │ └── test/ │ └── browser/ │ ├── component-stack-2.test.jsx │ ├── component-stack.test.jsx │ ├── debug-compat.test.jsx │ ├── debug-hooks.test.jsx │ ├── debug-suspense.test.jsx │ ├── debug.options.test.jsx │ ├── debug.test.jsx │ ├── fakeDevTools.js │ ├── prop-types.test.js │ ├── serializeVNode.test.jsx │ └── validateHookArgs.test.jsx ├── demo/ │ ├── contenteditable.jsx │ ├── context.jsx │ ├── devtools.jsx │ ├── fragments.jsx │ ├── index.html │ ├── index.jsx │ ├── key_bug.jsx │ ├── list.jsx │ ├── logger.jsx │ ├── mobx.jsx │ ├── nested-suspense/ │ │ ├── addnewcomponent.jsx │ │ ├── component-container.jsx │ │ ├── dropzone.jsx │ │ ├── editor.jsx │ │ ├── index.jsx │ │ └── subcomponent.jsx │ ├── old.js.bak │ ├── package.json │ ├── people/ │ │ ├── Readme.md │ │ ├── index.tsx │ │ ├── profile.tsx │ │ ├── router.tsx │ │ ├── store.ts │ │ └── styles/ │ │ ├── animations.scss │ │ ├── app.scss │ │ ├── avatar.scss │ │ ├── button.scss │ │ ├── index.scss │ │ └── profile.scss │ ├── preact.jsx │ ├── profiler.jsx │ ├── pythagoras/ │ │ ├── index.jsx │ │ └── pythagoras.jsx │ ├── redux-toolkit.jsx │ ├── redux.jsx │ ├── reduxUpdate.jsx │ ├── reorder.jsx │ ├── spiral.jsx │ ├── stateOrderBug.jsx │ ├── style.css │ ├── style.scss │ ├── styled-components.jsx │ ├── suspense-router/ │ │ ├── bye.jsx │ │ ├── hello.jsx │ │ ├── index.jsx │ │ └── simple-router.jsx │ ├── suspense.jsx │ ├── textFields.jsx │ ├── todo.jsx │ ├── tsconfig.json │ ├── vite.config.js │ └── zustand.jsx ├── devtools/ │ ├── LICENSE │ ├── mangle.json │ ├── package.json │ ├── src/ │ │ ├── devtools.js │ │ ├── index.d.ts │ │ └── index.js │ └── test/ │ └── browser/ │ └── addHookName.test.jsx ├── hooks/ │ ├── LICENSE │ ├── mangle.json │ ├── package.json │ ├── src/ │ │ ├── index.d.ts │ │ ├── index.js │ │ └── internal.d.ts │ └── test/ │ ├── _util/ │ │ └── useEffectUtil.js │ └── browser/ │ ├── combinations.test.jsx │ ├── componentDidCatch.test.jsx │ ├── errorBoundary.test.jsx │ ├── hooks.options.test.jsx │ ├── useCallback.test.jsx │ ├── useContext.test.jsx │ ├── useDebugValue.test.jsx │ ├── useEffect.test.jsx │ ├── useEffectAssertions.jsx │ ├── useId.test.jsx │ ├── useImperativeHandle.test.jsx │ ├── useLayoutEffect.test.jsx │ ├── useMemo.test.jsx │ ├── useReducer.test.jsx │ ├── useRef.test.jsx │ └── useState.test.jsx ├── jsconfig-lint.json ├── jsconfig.json ├── jsx-runtime/ │ ├── LICENSE │ ├── mangle.json │ ├── package.json │ ├── src/ │ │ ├── index.d.ts │ │ ├── index.js │ │ └── utils.js │ └── test/ │ └── browser/ │ └── jsx-runtime.test.js ├── mangle.json ├── package.json ├── scripts/ │ └── release/ │ ├── create-gh-release.js │ ├── publish.mjs │ └── upload-gh-asset.js ├── src/ │ ├── clone-element.js │ ├── component.js │ ├── constants.js │ ├── create-context.js │ ├── create-element.js │ ├── diff/ │ │ ├── catch-error.js │ │ ├── children.js │ │ ├── index.js │ │ └── props.js │ ├── dom.d.ts │ ├── index.d.ts │ ├── index.js │ ├── internal.d.ts │ ├── jsx.d.ts │ ├── options.js │ ├── render.js │ └── util.js ├── test/ │ ├── _util/ │ │ ├── dom.js │ │ ├── helpers.jsx │ │ ├── logCall.js │ │ └── optionSpies.js │ ├── browser/ │ │ ├── cloneElement.test.jsx │ │ ├── components.test.jsx │ │ ├── context.test.jsx │ │ ├── createContext.test.jsx │ │ ├── customBuiltInElements.test.jsx │ │ ├── events.test.jsx │ │ ├── focus.test.jsx │ │ ├── fragments.test.jsx │ │ ├── getDomSibling.test.jsx │ │ ├── hydrate.test.jsx │ │ ├── isValidElement.test.js │ │ ├── keys.test.jsx │ │ ├── lifecycles/ │ │ │ ├── componentDidCatch.test.jsx │ │ │ ├── componentDidMount.test.jsx │ │ │ ├── componentDidUpdate.test.jsx │ │ │ ├── componentWillMount.test.jsx │ │ │ ├── componentWillReceiveProps.test.jsx │ │ │ ├── componentWillUnmount.test.jsx │ │ │ ├── componentWillUpdate.test.jsx │ │ │ ├── getDerivedStateFromError.test.jsx │ │ │ ├── getDerivedStateFromProps.test.jsx │ │ │ ├── getSnapshotBeforeUpdate.test.jsx │ │ │ ├── lifecycle.test.jsx │ │ │ └── shouldComponentUpdate.test.jsx │ │ ├── mathml.test.jsx │ │ ├── placeholders.test.jsx │ │ ├── refs.test.jsx │ │ ├── render.test.jsx │ │ ├── select.test.jsx │ │ ├── spec.test.jsx │ │ ├── style.test.jsx │ │ ├── svg.test.jsx │ │ └── toChildArray.test.jsx │ ├── fixtures/ │ │ └── preact.js │ ├── node/ │ │ └── index.test.js │ ├── shared/ │ │ ├── createContext.test.jsx │ │ ├── createElement.test.jsx │ │ ├── exports.test.js │ │ ├── isValidElement.test.js │ │ └── isValidElementTests.jsx │ └── ts/ │ ├── Component.test.tsx │ ├── VNode.test.tsx │ ├── custom-elements.tsx │ ├── dom-attributes.test-d.tsx │ ├── hoc.test.tsx │ ├── jsx-namespace.test-d.tsx │ ├── package.json │ ├── preact-global.test-d.tsx │ ├── preact.tsx │ ├── refs.tsx │ └── tsconfig.json ├── test-utils/ │ ├── package.json │ ├── src/ │ │ ├── index.d.ts │ │ └── index.js │ └── test/ │ └── shared/ │ ├── act.test.jsx │ └── rerender.test.jsx ├── types/ │ └── weak-key.d.ts ├── vitest.config.mjs └── vitest.setup.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*] indent_style = tab end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [{*.json,.*rc,*.yml}] indent_style = space indent_size = 2 [*.md] trim_trailing_whitespace = false ================================================ FILE: .github/CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: - Using welcoming and inclusive language - Being respectful of differing viewpoints and experiences - Gracefully accepting constructive criticism - Focusing on what is best for the community - Showing empathy towards other community members Examples of unacceptable behavior by participants include: - The use of sexualized language or imagery and unwelcome sexual attention or advances - Trolling, insulting/derogatory comments, and personal or political attacks - Public or private harassment - Publishing others' private information, such as a physical or electronic address, without explicit permission - Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at hello@preactjs.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- - [ ] Check if updating to the latest Preact version resolves the issue **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** If possible, please provide a link to a StackBlitz/CodeSandbox/Codepen project or a GitHub repository that demonstrates the issue. You can use the following template on StackBlitz to get started: https://stackblitz.com/edit/create-preact-starter Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. See error **Expected behavior** What should have happened when following the steps above? ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: feature request assignees: '' --- **Describe the feature you'd love to see** A clear and concise description of what you'd love to see added to Preact. **Additional context (optional)** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/workflows/benchmarks.yml ================================================ name: Benchmarks on: workflow_dispatch: workflow_call: jobs: prepare: name: Prepare environment runs-on: ubuntu-latest timeout-minutes: 5 steps: - name: Download locally built preact package uses: actions/download-artifact@v4 with: name: npm-package - run: mv preact.tgz preact-local.tgz - name: Download base package uses: andrewiggins/download-base-artifact@v3 with: artifact: npm-package workflow: ci.yml required: true - run: mv preact.tgz preact-main.tgz - name: Upload locally build & base preact package uses: actions/upload-artifact@v4 with: name: bench-environment path: | preact-local.tgz preact-main.tgz bench_todo: name: Bench todo uses: ./.github/workflows/run-bench.yml needs: prepare with: benchmark: todo/todo timeout: 10 bench_text_update: name: Bench text-update uses: ./.github/workflows/run-bench.yml needs: prepare with: benchmark: text-update/text-update timeout: 10 bench_many_updates: name: Bench many-updates uses: ./.github/workflows/run-bench.yml needs: prepare with: benchmark: many-updates/many-updates timeout: 10 bench_replace1k: name: Bench replace1k uses: ./.github/workflows/run-bench.yml needs: prepare with: benchmark: table-app/replace1k bench_update10th1k: name: Bench 03_update10th1k_x16 uses: ./.github/workflows/run-bench.yml needs: prepare with: benchmark: table-app/update10th1k bench_create10k: name: Bench create10k uses: ./.github/workflows/run-bench.yml needs: prepare with: benchmark: table-app/create10k bench_hydrate1k: name: Bench hydrate1k uses: ./.github/workflows/run-bench.yml needs: prepare with: benchmark: table-app/hydrate1k bench_filter_list: name: Bench filter-list uses: ./.github/workflows/run-bench.yml needs: prepare with: benchmark: filter-list/filter-list timeout: 10 ================================================ FILE: .github/workflows/build-test.yml ================================================ name: Build & Test on: workflow_dispatch: workflow_call: inputs: ref: description: 'Branch or tag ref to check out' type: string required: false default: '' artifact_name: description: 'Name of the artifact to upload' type: string required: false default: 'npm-package' jobs: build_test: name: Build & Test runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: ref: ${{ inputs.ref || '' }} - uses: actions/setup-node@v4 with: node-version-file: 'package.json' cache: 'npm' cache-dependency-path: '**/package-lock.json' - run: npm ci - name: test env: CI: true COVERAGE: true FLAKEY: false # Not using `npm test` since it rebuilds source which npm ci has already done run: npm run lint && npm run test:unit - name: Coveralls GitHub Action uses: coverallsapp/github-action@v2.3.0 timeout-minutes: 2 with: github-token: ${{ secrets.GITHUB_TOKEN }} fail-on-error: false - name: Package # Use --ignore-scripts here to avoid re-building again before pack run: | npm pack --ignore-scripts mv preact-*.tgz preact.tgz - name: Upload npm package uses: actions/upload-artifact@v4 with: name: ${{ inputs.artifact_name || 'npm-package' }} path: preact.tgz ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: workflow_dispatch: pull_request: branches: - '**' push: branches: - main - restructure - v11 jobs: filter_jobs: name: Filter jobs runs-on: ubuntu-latest outputs: jsChanged: ${{ steps.filter.outputs.jsChanged }} steps: - uses: actions/checkout@v4 - uses: dorny/paths-filter@v3 id: filter with: # Should be kept in sync with the filter in the PR Reporter workflow predicate-quantifier: 'every' filters: | jsChanged: - '**/src/**/*.js' - '!devtools/src/devtools.js' compressed_size: name: Compressed Size needs: filter_jobs if: ${{ needs.filter_jobs.outputs.jsChanged == 'true' }} uses: ./.github/workflows/size.yml build_test: name: Build & Test needs: filter_jobs uses: ./.github/workflows/build-test.yml benchmarks: name: Benchmarks needs: build_test if: ${{ needs.filter_jobs.outputs.jsChanged == 'true' }} uses: ./.github/workflows/benchmarks.yml ================================================ FILE: .github/workflows/pr-reporter.yml ================================================ name: Report Results to PR on: # The pull_request event can't write comments for PRs from forks so using this # workflow_run workflow as a workaround workflow_run: workflows: ['CI'] types: - completed - requested jobs: filter_jobs: name: Filter jobs runs-on: ubuntu-latest if: | github.event.workflow_run.event == 'pull_request' outputs: jsChanged: ${{ steps.filter.outputs.jsChanged }} steps: - uses: actions/checkout@v4 # Warning: This is kinda gnarly and weird! GitHub doesn't expose the `pull_request` # data if the PR is from a fork, nor does it let you access that data via their API # (by querying for PRs associated with the commit or querying the commit itself, both # come up empty for forked commits). As such, to get the base SHA of the PR we need to # query for all PRs on the repo and match the owner+branch to find the right one and # then extract the base SHA from that. # # I see no reason why GitHub shouldn't expose this, it's just an SHA, branch name, and # URL, but they don't so we're doing it this way. Hopefully we can remove this one day. - name: Get PR Base SHA id: get_pr_base_sha env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} FORK_OWNER: ${{ github.event.workflow_run.head_repository.owner.login }} FORK_BRANCH: ${{ github.event.workflow_run.head_branch }} run: | set -euo pipefail PR_JSON=$(gh api "repos/preactjs/preact/pulls?state=all&head=$FORK_OWNER:$FORK_BRANCH") BASE_SHA=$(jq -r '.[0].base.sha' <<< "$PR_JSON") echo "base_sha=$BASE_SHA" >> "$GITHUB_OUTPUT" - uses: dorny/paths-filter@v3 id: filter with: # As this Workflow is triggered by a `workflow_run` event, the filter action # can't automatically assume we're working with PR data. As such, we need to # wire it up manually with a base (merge target) and ref (source branch). base: ${{ steps.get_pr_base_sha.outputs.base_sha }} ref: ${{ github.event.workflow_run.head_sha }} # Should be kept in sync with the filter in the CI workflow predicate-quantifier: 'every' filters: | jsChanged: - '**/src/**/*.js' - '!devtools/src/devtools.js' report_running: name: Report benchmarks are in-progress needs: filter_jobs runs-on: ubuntu-latest # Only add the "benchmarks are running" text when a workflow_run is # requested (a.k.a starting) if: | needs.filter_jobs.outputs.jsChanged == 'true' && github.event.action == 'requested' steps: - name: Report Tachometer Running uses: andrewiggins/tachometer-reporter-action@v2 with: # Set initialize to true so this action just creates the comment or # adds the "benchmarks are running" text initialize: true report_results: name: Report benchmark results needs: filter_jobs runs-on: ubuntu-latest # Only run this job if the event action was "completed" and the triggering # workflow_run was successful if: | needs.filter_jobs.outputs.jsChanged == 'true' && github.event.action == 'completed' && github.event.workflow_run.conclusion == 'success' steps: # Download the artifact from the triggering workflow that contains the # Tachometer results to report - uses: dawidd6/action-download-artifact@v2 with: workflow: ${{ github.event.workflow.id }} run_id: ${{ github.event.workflow_run.id }} name_is_regexp: true name: results-* path: results # Create/update the comment with the latest results - name: Report Tachometer Results uses: andrewiggins/tachometer-reporter-action@v2 with: path: results/**/*.json base-bench-name: preact-main pr-bench-name: preact-local summarize: 'duration, usedJSHeapSize' ================================================ FILE: .github/workflows/release.yml ================================================ name: Release on: create jobs: build: if: github.ref_type == 'tag' uses: preactjs/preact/.github/workflows/build-test.yml@main release: runs-on: ubuntu-latest needs: build steps: - uses: actions/checkout@v4 - uses: actions/download-artifact@v4 with: name: npm-package - name: Create draft release id: create-release uses: actions/github-script@v6 with: script: | const script = require('./scripts/release/create-gh-release.js') return script({ github, context }) - name: Upload release artifact uses: actions/github-script@v6 with: script: | const script = require('./scripts/release/upload-gh-asset.js') return script({ require, github, context, glob, release: ${{ steps.create-release.outputs.result }} }) ================================================ FILE: .github/workflows/run-bench.yml ================================================ name: Benchmark Worker # Expectations: # # This workflow expects calling workflows to have uploaded an artifact named # "bench-environment" that contains any built artifacts required to run the # benchmark. This typically is the dist/ folder that running `npm run build` # produces and/or a tarball of a previous build to bench the local build against on: workflow_call: inputs: benchmark: description: 'The name of the benchmark to run. Should be name of an HTML file without the .html extension' type: string required: true trace: description: 'Whether to capture browser traces for this benchmark run' type: boolean required: false default: false timeout: description: 'How many minutes to give the benchmark to run before timing out and failing' type: number required: false default: 20 jobs: run_bench: name: Bench ${{ inputs.benchmark }} runs-on: ubuntu-latest timeout-minutes: ${{ inputs.timeout }} steps: - uses: actions/checkout@v4 with: submodules: 'recursive' - uses: actions/setup-node@v4 with: node-version-file: 'package.json' cache: 'npm' cache-dependency-path: '**/package-lock.json' # Setup pnpm - name: Install pnpm uses: pnpm/action-setup@v3 with: version: 8 run_install: false - name: Get pnpm store directory id: pnpm-cache run: | echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT - uses: actions/cache@v4 name: Setup pnpm cache with: path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }} key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}-pnpm-store- # Install benchmark dependencies - uses: actions/download-artifact@v4 with: name: bench-environment - name: Move tarballs from env to correct location run: | ls -al mv preact-local.tgz benchmarks/dependencies/preact/local-pinned/preact-local-pinned.tgz ls -al benchmarks/dependencies/preact/local-pinned mv preact-main.tgz benchmarks/dependencies/preact/main/preact-main.tgz ls -al benchmarks/dependencies/preact/main - name: Install deps working-directory: benchmarks # Set the CHROMEDRIVER_FILEPATH so the chromedriver npm package uses the # correct binary when its installed run: | export CHROMEDRIVER_FILEPATH=$(which chromedriver) pnpm install # Install local dependencies with --no-frozen-lockfile to ensure local tarballs # are installed regardless of if they match the integrity hash stored in the lockfile pnpm install --no-frozen-lockfile --filter ./dependencies # Run benchmark - name: Run benchmark working-directory: benchmarks run: | export CHROMEDRIVER_FILEPATH=$(which chromedriver) pnpm run bench apps/${{ inputs.benchmark }}.html -d preact@local-pinned -d preact@main --trace=${{ inputs.trace }} # Prepare output - name: Anaylze logs if present working-directory: benchmarks run: '[ -d out/logs ] && pnpm run analyze ${{ inputs.benchmark }} || echo "No logs to analyze"' - name: Tar logs if present working-directory: benchmarks run: | if [ -d out/logs ]; then LOGS_FILE=out/${{ inputs.benchmark }}_logs.tgz mkdir -p $(dirname $LOGS_FILE) tar -zcvf $LOGS_FILE out/logs else echo "No logs found" fi # Upload results and logs - name: Calculate log artifact name id: log-artifact-name run: | NAME=$(echo "${{ inputs.benchmark }}" | sed -r 's/[\/]+/_/g') echo "clean_name=$NAME" >> $GITHUB_OUTPUT echo "artifact_name=logs_$NAME" >> $GITHUB_OUTPUT - name: Upload results uses: actions/upload-artifact@v4 with: name: results-${{ steps.log-artifact-name.outputs.clean_name }} path: benchmarks/out/results/${{ inputs.benchmark }}.json - name: Upload logs uses: actions/upload-artifact@v4 with: name: ${{ steps.log-artifact-name.outputs.artifact_name }} path: benchmarks/out/${{ inputs.benchmark }}_logs.tgz if-no-files-found: ignore ================================================ FILE: .github/workflows/single-bench.yml ================================================ name: Benchmark Debug on: workflow_dispatch: inputs: benchmark: description: 'Which benchmark to run' type: choice options: - table-app/replace1k - table-app/update10th1k - table-app/create10k - table-app/hydrate1k - filter_list/filter-list - many-updates/many-updates - text-update/text-update - todo/todo required: true base: description: 'The branch name, tag, or commit sha of the version of preact to benchmark against.' type: string default: main required: false trace: description: 'Whether to capture browser traces for this benchmark run' type: boolean default: true required: false # A bug in GitHub actions prevents us from passing numbers (as either # number or string type) to called workflows. So disabling this for now. # See: https://github.com/orgs/community/discussions/67182 # # timeout: # description: 'How many minutes to give the benchmark to run before timing out and failing' # type: number # default: 20 # required: false jobs: build_local: name: Build local package uses: ./.github/workflows/ci.yml build_base: name: Build base package uses: ./.github/workflows/ci.yml with: ref: ${{ inputs.base }} artifact_name: base-npm-package prepare: name: Prepare environment runs-on: ubuntu-latest needs: - build_local - build_base timeout-minutes: 5 steps: - name: Download locally built preact package uses: actions/download-artifact@v4 with: name: npm-package - run: mv preact.tgz preact-local.tgz - name: Clear working directory run: | ls -al rm -rf * echo "====================" ls -al - name: Download base package uses: actions/download-artifact@v4 with: name: base-npm-package - run: mv preact.tgz preact-main.tgz - name: Upload locally built & base preact package uses: actions/upload-artifact@v4 with: name: bench-environment path: | preact-local.tgz preact-main.tgz benchmark: name: Bench ${{ inputs.benchmark }} uses: ./.github/workflows/run-bench.yml needs: prepare with: benchmark: ${{ inputs.benchmark }} trace: ${{ inputs.trace }} # timeout: ${{ inputs.timeout }} ================================================ FILE: .github/workflows/size.yml ================================================ name: Compressed Size on: workflow_call: jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version-file: 'package.json' cache: 'npm' cache-dependency-path: '**/package-lock.json' - uses: preactjs/compressed-size-action@v2 with: repo-token: '${{ secrets.GITHUB_TOKEN }}' # Our `prepare` script already builds the app post-install, # building it again would be redundant build-script: 'npm run --if-present noop' ================================================ FILE: .gitignore ================================================ .DS_Store node_modules npm-debug.log dist */package-lock.json yarn.lock .vscode .idea test/ts/**/*.js coverage *.sw[op] *.log package/ preact-*.tgz preact.tgz *.local.* ================================================ FILE: .gitmodules ================================================ [submodule "benchmarks"] path = benchmarks url = https://github.com/preactjs/benchmarks ================================================ FILE: .husky/pre-commit ================================================ npx nano-staged ================================================ FILE: .oxlintrc.json ================================================ { "$schema": "./node_modules/oxlint/configuration_schema.json", "ignorePatterns": [ "**/dist/**", "benchmarks/**" ], "rules": { "camelcase": [ 1, { "allow": ["__test__*", "unstable_*", "UNSAFE_*"] } ], "no-unused-vars": [ 2, { "args": "none", "caughtErrors": "none", "varsIgnorePattern": "^h|React|createElement|Fragment$" } ], "typescript/no-namespace": 0, "no-constant-binary-expression": 0, "no-useless-catch": 0, "no-empty-pattern": 0, "prefer-rest-params": 0, "prefer-spread": 0, "no-cond-assign": 0, "react/jsx-no-bind": 0, "react/no-danger": 0, "react/no-danger-with-children": 0, "react/prefer-stateless-function": 0, "react/sort-comp": 0, "jest/valid-expect": 0, "jest/no-disabled-tests": 0, "jest/no-test-callback": 0, "jest/expect-expect": 0, "jest/no-standalone-expect": 0, "jest/no-export": 0, "react/no-find-dom-node": 0, "react/no-direct-mutation-state": 0, "react/no-children-prop": 0, "react/jsx-key": 0, "react/no-string-refs": 0, "react/require-render-return": 0, "unicorn/no-new-array": 0, "unicorn/prefer-string-starts-ends-with": 0 } } ================================================ FILE: .prettierignore ================================================ .DS_Store node_modules npm-debug.log dist */package-lock.json yarn.lock .vscode .idea test/ts/**/*.js coverage *.sw[op] *.log package/ preact-*.tgz preact.tgz package-lock.json ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing This document is intended for developers interested in making contributions to Preact and documents our internal processes like releasing a new version. ## Getting Started These steps will help you set up your development environment. That includes all dependencies we use to build Preact and developer tooling like git commit hooks. 1. Clone the git repository: `git clone git@github.com:preactjs/preact.git` 2. Go into the cloned folder: `cd preact/` 3. Install all dependencies: `npm install` ## The Repo Structure This repository contains Preact itself, as well as several addons like the debugging package for example. This is reflected in the directory structure of this repository. Each package has a `src/` folder where the source code can be found, a `test` folder for all sorts of tests that check if the code in `src/` is correct, and a `dist/` folder where you can find the bundled artifacts. Note that the `dist/` folder may not be present initially. It will be created as soon as you run any of the build scripts inside `package.json`. More on that later ;) A quick overview of our repository: ```bash # The repo root (folder where you cloned the repo into) / src/ # Source code of our core test/ # Unit tests for core dist/ # Build artifacts for publishing on npm (may not be present) # Sub-package, can be imported via `preact/compat` by users. # Compat stands for react-compatibility layer which tries to mirror the # react API as close as possible (mostly legacy APIs) compat/ src/ # Source code of the compat addon test/ # Tests related to the compat addon dist/ # Build artifacts for publishing on npm (may not be present) # Sub-package, can be imported via `preact/hooks` by users. # The hooks API is an effect based API to deal with component lifecycles. # It's similar to hooks in React hooks/ src/ # Source code of the hooks addon test/ # Tests related to the hooks addon dist/ # Build artifacts for publishing on npm (may not be present) # Sub-package, can be imported via `preact/debug` by users. # Includes debugging warnings and error messages for common mistakes found # in Preact applications. Also hosts the devtools bridge debug/ src/ # Source code of the debug addon test/ # Tests related to the debug addon dist/ # Build artifacts for publishing on npm (may not be present) # Sub-package, can be imported via `preact/test-utils` by users. # Provides helpers to make testing Preact applications easier test-utils/ src/ # Source code of the test-utils addon test/ # Tests related to the test-utils addon dist/ # Build artifacts for publishing on npm (may not be present) # A demo application that we use to debug tricky errors and play with new # features. demo/ # Contains build scripts and dependencies for development package.json ``` _Note: The code for rendering Preact on the server lives in another repo and is a completely separate npm package. It can be found here: [https://github.com/preactjs/preact-render-to-string](https://github.com/preactjs/preact-render-to-string)_ ### What does `mangle.json` do? It's a special file that can be used to specify how `terser` (previously known as `uglify`) will minify variable names. Because each sub-package has its own distribution files we need to ensure that the variable names stay consistent across bundles. ## What does `options.js` do? Unique to Preact we do support several ways to hook into our renderer. All our addons use that to inject code at different stages of a render process. They are documented in our typings in `internal.d.ts`. The core itself doesn't make use of them, which is why the file only contains an empty `object`. ## Important Branches We have a couple of important branches to be aware of: - `main` - This is the main development branch and represents the upcoming v11 release line. - `v10.x` - This branch represents the current stable release line, v10. As we have yet to release v11, contributors are welcome to use either branch to build upon. We will try to port changes between the branches when possible, to keep them in sync, but if you're feeling generous, we'd love if you'd submit PRs to both branches! ## Creating your first Pull-Request We try to make it as easy as possible to contribute to Preact and make heavy use of GitHub's "Draft PR" feature which tags Pull-Requests (short = PR) as work in progress. PRs tend to be published as soon as there is an idea that the developer deems worthwhile to include into Preact and has written some rough code. The PR doesn't have to be perfect or anything really ;) Once a PR or a Draft PR has been created our community typically joins the discussion about the proposed change. Sometimes that includes ideas for test cases or even different ways to go about implementing a feature. Often this also includes ideas on how to make the code smaller. We usually refer to the latter as "code-golfing" or just "golfing". When everything is good to go someone will approve the PR and the changes will be merged into the `main` or `v10.x` branches and we usually cut a release a few days/ a week later. _The big takeaway for you here is, that we will guide you along the way. We're here to help to make a PR ready for approval!_ The short summary is: 1. Make changes and submit a PR 2. Modify change according to feedback (if there is any) 3. PR will be merged into `main` or `v10.x` 4. A new release will be cut (every 2-3 weeks). ## Commonly used scripts for contributions Scripts can be executed via `npm run [script]`. - `build` - compiles all packages ready for publishing to npm - `build:core` - builds just Preact itself - `build:debug` - builds the debug addon only - `build:devtools` - builds the devtools addon only - `build:hooks` - builds the hook addon only - `build:test-utils` - builds the test-utils addon only - `build:compat` - builds the compat addon only - `build:jsx` - builds the JSX runtime addon only - `test` - Run all tests (linting, TypeScript definitions, unit/integration tests) - `test:ts` - Run all tests for TypeScript definitions - `test:vitest` - Run all unit/integration tests. - `test:vitest:watch` - Same as above, but it will automatically re-run the test suite if a code change was detected. But to be fair, the ones we mostly use locally are `build` and `test:vitest:watch`. The other ones are mainly used on our CI pipeline. _Note: Both `test:vitest` and `test:vitest:watch` listen to the environment variable `COVERAGE=true`. Disabling code coverage can significantly speed up the time it takes to complete the test suite._ _Note2: Individual tests can be executed by appending `.only`:_ ```jsx it.only('should test something', () => { expect(1).to.equal(1); }); ``` ## Common terminology and variable names - `vnode` -> shorthand for `virtual-node` which is an object that specifies how a Component or DOM-node looks like - `commit` -> A commit is the moment in time when you flush all changes to the DOM - `c` -> The variable `c` always refers to a `component` instance throughout our code base. - `diff/diffing` -> Diffing describes the process of comparing two "things". In our case we compare the previous `vnode` tree with the new one and apply the delta to the DOM. - `root` -> The topmost node of a `vnode` tree ## Tips for getting to know the code base - Check the JSDoc block right above the function definition to understand what it does. It contains a short description of each function argument and what it does. - Check the callsites of a function to understand how it's used. Modern editors/IDEs allow you to quickly find those, or use the plain old search feature instead. ## Benchmarks We have a benchmark suite that we use to measure the performance of Preact. Our benchmark suite lives in our [preactjs/benchmarks repository](https://github.com/preactjs/benchmarks), but is included here as Git submodule. To run the benchmarks, first ensure [PNPM](https://pnpm.io/installation) is installed on your system and initialize and setup the submodule (it uses `pnpm` as a package manager): ```bash pnpm -v # Make sure pnpm is installed git submodule update --init --recursive cd benchmarks pnpm i ``` Then you can run the benchmarks: ```bash # In the benchmarks folder pnpm run bench ``` Checkout the README in the benchmarks folder for more information on running benchmarks. > **Note:** When switching branches, git submodules are not automatically updated to the commit of the new branch - it stays at the commit of the previous branch. This can be a feature! It allows you to work in different branches with the latest versions of the benchmarks - especially if you have made changes to the benchmarks. > > However if you want to switch branches and also update the benchmarks to the latest commit of the new branch, you can run `git submodule update --recursive` after switching branches, or run `git checkout --recurse-submodules` when checking out a new branch. ## FAQ ### Why does the JSDoc use TypeScript syntax to specify types? Several members of the team are very fond of TypeScript and we wanted to leverage as many of its advantages, like improved autocompletion, for Preact. We even attempted to port Preact to TypeScript a few times, but we ran into many issues with the DOM typings. Those would force us to fill our codebase with many `any` castings, making our code very noisy. Luckily TypeScript has a mode where it can somewhat reliably typecheck JavaScript code by reusing the types defined in JSDoc blocks. It's not perfect and it often has trouble inferring the correct types the further one strays away from the function arguments, but it's good enough that it helps us a lot with autocompletion. Another plus is that we can make sure that our TypeScript definitions are correct at the same time. Check out the [official TypeScript documentation](https://www.typescriptlang.org/docs/handbook/type-checking-javascript-files.html) for more information. _Note that we have separate tests for our TypeScript definition files. We only use `ts-check` for local development and don't check it anywhere else like on the CI._ ## How to create a good bug report To be able to fix issues we need to see them on our machine. This is only possible when we can reproduce the error. The easiest way to do that is narrow down the problem to specific components or combination of them. This can be done by removing as much unrelated code as possible. The perfect way to do that is to make a [codesandbox](https://codesandbox.io/). That way you can easily share the problematic code and ensure that others can see the same issue you are seeing. For us a [codesandbox](https://codesandbox.io/) says more than a 1000 words :tada: ## I have more questions on how to contribute to Preact. How can I reach you? We closely watch our issues and have a pretty active [Slack workspace](https://chat.preactjs.com/). Nearly all our communication happens via these two forms of communication. ## Releasing Preact (Maintainers only) This guide is intended for core team members that have the necessary rights to publish new releases on npm. 1. Make a PR where **only** the version number is incremented in `package.json` and everywhere else. A simple search and replace works. (note: We follow `SemVer` conventions) 2. Wait until the PR is approved and merged. 3. Switch back to the `main` branch and pull the merged PR 4. Create and push a tag for the new version you want to publish: 1. `git tag 10.0.0` 2. `git push --tags` 5. Wait for the Release workflow to complete - It'll create a draft release and upload the built npm package as an asset to the release 6. [Fill in the release notes](#writing-release-notes) in GitHub and publish them 7. Run the publish script with the tag you created 1. `node ./scripts/release/publish.mjs 10.0.0` 2. Make sure you have 2FA enabled in npm, otherwise the above command will fail. 3. If you're doing a pre-release add `--npm-tag next` to the `publish.mjs` command to publish it under a different tag (default is `latest`) 8. Tweet it out ## Legacy Releases (8.x) > **ATTENTION:** Make sure that you've cleared the project correctly > when switching from a 10.x branch. 0. Run `rm -rf dist node_modules && npm i` to make sure to have the correct dependencies. 1. [Write the release notes](#writing-release-notes) and keep them as a draft in GitHub 1. I'd recommend writing them in an offline editor because each edit to a draft will change the URL in GitHub. 2. Make a PR where **only** the version number is incremented in `package.json` (note: We follow `SemVer` conventions) 3. Wait until the PR is approved and merged. 4. Switch back to the `main` branch and pull the merged PR 5. Run `npm run build && npm publish` 1. Make sure you have 2FA enabled in npm, otherwise the above command will fail. 2. If you're doing a pre-release add `--tag next` to the `npm publish` command to publish it under a different tag (default is `latest`) 6. Publish the release notes and create the correct git tag. 7. Tweet it out ## Writing release notes The release notes have become a sort of tiny blog post about what's happening in preact-land. The title usually has this format: ```txt Version Name ``` Example: ```txt 10.0.0-beta.1 Los Compresseros ``` The name is optional, we just have fun finding creative names :wink: To keep them interesting we try to be as concise as possible and to just reflect where we are. There are some rules we follow while writing them: - Be nice, use a positive tone. Avoid negative words - Show, don't just tell. - Be honest. - Don't write too much, keep it simple and short. - Avoid making promises and don't overpromise. That leads to unhappy users - Avoid framework comparisons if possible - Highlight awesome community contributions (or great issue reports) - If in doubt, praise the users. After this section we typically follow with a changelog part that's divided into 4 groups in order of importance for the user: - Features - Bug Fixes - Typings - Maintenance We generate it via this handy cli program: [changelogged](https://github.com/marvinhagemeister/changelogged). It will collect and format the descriptions of all PRs that have been merged between two tags. The usual command is `changelogged 10.0.0-rc.2..HEAD` similar to how you'd diff two points in time with git. This will get you 90% there, but you still need to divide it into groups. It's also a good idea to unify the formatting of the descriptions, so that they're easier to read and don't look like a mess. ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2015-present Jason Miller Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ > [!NOTE] > This is the branch for the upcoming release, for patches to v10 you need the [v10.x branch](https://github.com/preactjs/preact/tree/v10.x)

![Preact](https://raw.githubusercontent.com/preactjs/preact/8b0bcc927995c188eca83cba30fbc83491cc0b2f/logo.svg?sanitize=true 'Preact')

Fast 4kB alternative to React with the same modern API.

**All the power of Virtual DOM components, without the overhead:** - Familiar React API & patterns: ES6 Class, hooks, and Functional Components - Extensive React compatibility via a simple [preact/compat] alias - Everything you need: JSX, VDOM, [DevTools], HMR, SSR. - Highly optimized diff algorithm and seamless hydration from Server Side Rendering - Supports all modern browsers - Transparent asynchronous rendering with a pluggable scheduler ### 💁 More information at the [Preact Website ➞](https://preactjs.com)
[![npm](https://img.shields.io/npm/v/preact.svg)](https://www.npmjs.com/package/preact) [![Preact Slack Community](https://img.shields.io/badge/Slack%20Community-preact.slack.com-blue)](https://chat.preactjs.com) [![OpenCollective Backers](https://opencollective.com/preact/backers/badge.svg)](#backers) [![OpenCollective Sponsors](https://opencollective.com/preact/sponsors/badge.svg)](#sponsors) [![coveralls](https://img.shields.io/coveralls/preactjs/preact/main.svg)](https://coveralls.io/github/preactjs/preact) [![gzip size](https://img.badgesize.io/https://unpkg.com/preact/dist/preact.min.js?compression=gzip&label=gzip)](https://unpkg.com/preact/dist/preact.min.js) [![brotli size](https://img.badgesize.io/https://unpkg.com/preact/dist/preact.min.js?compression=brotli&label=brotli)](https://unpkg.com/preact/dist/preact.min.js)
You can find some awesome libraries in the [awesome-preact list](https://github.com/preactjs/awesome-preact) :sunglasses: --- ## Getting Started > 💁 _**Note:** You [don't need ES2015 to use Preact](https://github.com/developit/preact-in-es3)... but give it a try!_ #### Tutorial: Building UI with Preact With Preact, you create user interfaces by assembling trees of components and elements. Components are functions or classes that return a description of what their tree should output. These descriptions are typically written in [JSX](https://react.dev/learn/writing-markup-with-jsx) (shown underneath), or [HTM](https://github.com/developit/htm) which leverages standard JavaScript Tagged Templates. Both syntaxes can express trees of elements with "props" (similar to HTML attributes) and children. To get started using Preact, first look at the render() function. This function accepts a tree description and creates the structure described. Next, it appends this structure to a parent DOM element provided as the second argument. Future calls to render() will reuse the existing tree and update it in-place in the DOM. Internally, render() will calculate the difference from previous outputted structures in an attempt to perform as few DOM operations as possible. ```js import { h, render } from 'preact'; // Tells babel to use h for JSX. It's better to configure this globally. // See https://babeljs.io/docs/en/babel-plugin-transform-react-jsx#usage // In tsconfig you can specify this with the jsxFactory /** @jsx h */ // create our tree and append it to document.body: render(

Hello

, document.body ); // update the tree in-place: render(

Hello World!

, document.body ); // ^ this second invocation of render(...) will use a single DOM call to update the text of the

``` Hooray! render() has taken our structure and output a User Interface! This approach demonstrates a simple case, but would be difficult to use as an application grows in complexity. Each change would be forced to calculate the difference between the current and updated structure for the entire application. Components can help here – by dividing the User Interface into nested Components each can calculate their difference from their mounted point. Here's an example: ```js import { render, h } from 'preact'; import { useState } from 'preact/hooks'; /** @jsx h */ const App = () => { const [input, setInput] = useState(''); return (

Do you agree to the statement: "Preact is awesome"?

setInput(e.target.value)} />
); }; render(, document.body); ``` --- ## Sponsors Become a sponsor and get your logo on our README on GitHub with a link to your site. [[Become a sponsor](https://opencollective.com/preact#sponsor)]             ## Backers Support us with a monthly donation and help us continue our activities. [[Become a backer](https://opencollective.com/preact#backer)] --- ## License MIT [![Preact](https://i.imgur.com/YqCHvEW.gif)](https://preactjs.com) [preact/compat]: https://github.com/preactjs/preact/tree/main/compat [hyperscript]: https://github.com/dominictarr/hyperscript [DevTools]: https://github.com/preactjs/preact-devtools ================================================ FILE: babel.config.js ================================================ module.exports = function (api) { api.cache(true); const noModules = String(process.env.BABEL_NO_MODULES) === 'true'; const rename = {}; const mangle = require('./mangle.json'); for (let prop in mangle.props.props) { let name = prop; if (name[0] === '$') { name = name.slice(1); } rename[name] = mangle.props.props[prop]; } return { presets: [ [ '@babel/preset-env', { loose: true, // Don't transform modules when using esbuild modules: noModules ? false : 'auto', exclude: ['@babel/plugin-transform-typeof-symbol'], targets: { browsers: [ 'chrome >= 40', 'safari >= 9', 'firefox >= 36', 'edge >= 12', 'not dead' ] } } ] ], plugins: [ '@babel/plugin-transform-react-jsx', ['babel-plugin-transform-rename-properties', { rename }] ], include: ['**/src/**/*.js', '**/test/**/*.js'], overrides: [ { test: /(component-stack|debug)\.test\.js$/, plugins: ['@babel/plugin-transform-react-jsx-source'] } ] }; }; ================================================ FILE: biome.json ================================================ { "formatter": { "enabled": true, "formatWithErrors": false, "indentStyle": "tab", "indentWidth": 2, "lineEnding": "lf", "lineWidth": 80, "attributePosition": "auto", "includes": [ "**", "!**/.DS_Store", "!**/node_modules", "!**/npm-debug.log", "!**/dist", "!**/*/package-lock.json", "!**/yarn.lock", "!**/.vscode", "!**/.idea", "!**/test/ts/**/*.js", "!**/coverage", "!**/*.sw\\[op\\]", "!**/*.log", "!**/package/", "!**/preact-*.tgz", "!**/preact.tgz", "!**/package-lock.json" ] }, "assist": { "actions": { "source": { "organizeImports": "on" } } }, "linter": { "enabled": true, "rules": { "recommended": true } }, "javascript": { "formatter": { "jsxQuoteStyle": "double", "quoteProperties": "asNeeded", "trailingCommas": "none", "semicolons": "always", "arrowParentheses": "asNeeded", "bracketSpacing": true, "bracketSameLine": false, "quoteStyle": "single", "attributePosition": "auto" } }, "overrides": [ { "includes": ["**/*.json", "**/.*rc/**", "**/*.yml"], "formatter": { "indentWidth": 2, "indentStyle": "space" } } ] } ================================================ FILE: compat/LICENSE ================================================ The MIT License (MIT) Copyright (c) 2015-present Jason Miller Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: compat/client.d.ts ================================================ import * as preact from '../src/index'; export function createRoot(container: preact.ContainerNode): { render(children: preact.ComponentChild): void; unmount(): void; }; export function hydrateRoot( container: preact.ContainerNode, children: preact.ComponentChild ): ReturnType; ================================================ FILE: compat/client.js ================================================ const { render, hydrate, unmountComponentAtNode } = require('preact/compat'); function createRoot(container) { return { // eslint-disable-next-line render: function (children) { render(children, container); }, // eslint-disable-next-line unmount: function () { unmountComponentAtNode(container); } }; } exports.createRoot = createRoot; exports.hydrateRoot = function (container, children) { hydrate(children, container); return createRoot(container); }; ================================================ FILE: compat/client.mjs ================================================ import { render, hydrate, unmountComponentAtNode } from 'preact/compat'; export function createRoot(container) { return { // eslint-disable-next-line render: function (children) { render(children, container); }, // eslint-disable-next-line unmount: function () { unmountComponentAtNode(container); } }; } export function hydrateRoot(container, children) { hydrate(children, container); return createRoot(container); } export default { createRoot, hydrateRoot }; ================================================ FILE: compat/jsx-dev-runtime.js ================================================ require('preact/compat'); module.exports = require('preact/jsx-runtime'); ================================================ FILE: compat/jsx-dev-runtime.mjs ================================================ import 'preact/compat'; export * from 'preact/jsx-runtime'; ================================================ FILE: compat/jsx-runtime.js ================================================ require('preact/compat'); module.exports = require('preact/jsx-runtime'); ================================================ FILE: compat/jsx-runtime.mjs ================================================ import 'preact/compat'; export * from 'preact/jsx-runtime'; ================================================ FILE: compat/mangle.json ================================================ { "help": { "what is this file?": "It controls protected/private property mangling so that minified builds have consistent property names.", "why are there duplicate minified properties?": "Most properties are only used on one type of objects, so they can have the same name since they will never collide. Doing this reduces size." }, "minify": { "mangle": { "properties": { "regex": "^_[^_]", "reserved": [ "__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED", "__REACT_DEVTOOLS_GLOBAL_HOOK__", "__PREACT_DEVTOOLS__", "_renderers", "__source", "__self" ] } } } } ================================================ FILE: compat/package.json ================================================ { "name": "preact-compat", "amdName": "preactCompat", "private": true, "description": "A React compatibility layer for Preact", "main": "dist/compat.js", "module": "dist/compat.mjs", "umd:main": "dist/compat.umd.js", "source": "src/index.js", "types": "src/index.d.ts", "license": "MIT", "mangle": { "regex": "^_" }, "peerDependencies": { "preact": "^10.0.0" } } ================================================ FILE: compat/scheduler.d.ts ================================================ export var unstable_ImmediatePriority: number; export var unstable_UserBlockingPriority: number; export var unstable_NormalPriority: number; export var unstable_LowPriority: number; export var unstable_IdlePriority: number; export function unstable_runWithPriority( priority: number, callback: () => void ): void; export var unstable_now: DOMHighResTimeStamp; ================================================ FILE: compat/scheduler.js ================================================ // see scheduler.mjs function unstable_runWithPriority(priority, callback) { return callback(); } module.exports = { unstable_ImmediatePriority: 1, unstable_UserBlockingPriority: 2, unstable_NormalPriority: 3, unstable_LowPriority: 4, unstable_IdlePriority: 5, unstable_runWithPriority, unstable_now: performance.now.bind(performance) }; ================================================ FILE: compat/scheduler.mjs ================================================ /* eslint-disable */ // This file includes experimental React APIs exported from the "scheduler" // npm package. Despite being explicitely marked as unstable some libraries // already make use of them. This file is not a full replacement for the // scheduler package, but includes the necessary shims to make those libraries // work with Preact. export var unstable_ImmediatePriority = 1; export var unstable_UserBlockingPriority = 2; export var unstable_NormalPriority = 3; export var unstable_LowPriority = 4; export var unstable_IdlePriority = 5; /** * @param {number} priority * @param {() => void} callback */ export function unstable_runWithPriority(priority, callback) { return callback(); } export var unstable_now = performance.now.bind(performance); ================================================ FILE: compat/server.browser.js ================================================ import { renderToString } from 'preact-render-to-string'; export { renderToString, renderToString as renderToStaticMarkup } from 'preact-render-to-string'; export default { renderToString, renderToStaticMarkup: renderToString }; ================================================ FILE: compat/server.d.ts ================================================ // @ts-nocheck TS loses its mind over the mixed module systems here. // It's not ideal, but works at runtime and we're not shipping mixed type definitions. import { renderToString } from 'preact-render-to-string'; import { renderToPipeableStream } from 'preact-render-to-string/stream-node'; import { renderToReadableStream } from 'preact-render-to-string/stream'; export { renderToString, renderToString as renderToStaticMarkup } from 'preact-render-to-string'; export { renderToPipeableStream } from 'preact-render-to-string/stream-node'; export { renderToReadableStream } from 'preact-render-to-string/stream'; export = { renderToString: typeof renderToString, renderToStaticMarkup: typeof renderToString, renderToPipeableStream: typeof renderToPipeableStream, renderToReadableStream: typeof renderToReadableStream }; ================================================ FILE: compat/server.js ================================================ /* eslint-disable */ var renderToString; try { const mod = require('preact-render-to-string'); renderToString = mod.default || mod.renderToString || mod; } catch (e) { throw Error( 'renderToString() error: missing "preact-render-to-string" dependency.' ); } var renderToReadableStream; try { const mod = require('preact-render-to-string/stream'); renderToReadableStream = mod.default || mod.renderToReadableStream || mod; } catch (e) { throw Error( 'renderToReadableStream() error: update "preact-render-to-string" dependency to at least 6.5.0.' ); } var renderToPipeableStream; try { const mod = require('preact-render-to-string/stream-node'); renderToPipeableStream = mod.default || mod.renderToPipeableStream || mod; } catch (e) { throw Error( 'renderToPipeableStream() error: update "preact-render-to-string" dependency to at least 6.5.0.' ); } module.exports = { renderToString: renderToString, renderToStaticMarkup: renderToString, renderToPipeableStream: renderToPipeableStream, renderToReadableStream: renderToReadableStream }; ================================================ FILE: compat/server.mjs ================================================ import { renderToString } from 'preact-render-to-string'; import { renderToPipeableStream } from 'preact-render-to-string/stream-node'; import { renderToReadableStream } from 'preact-render-to-string/stream'; export { renderToString, renderToString as renderToStaticMarkup } from 'preact-render-to-string'; export { renderToPipeableStream } from 'preact-render-to-string/stream-node'; export { renderToReadableStream } from 'preact-render-to-string/stream'; export default { renderToString, renderToStaticMarkup: renderToString, renderToPipeableStream, renderToReadableStream }; ================================================ FILE: compat/src/Children.js ================================================ import { toChildArray } from 'preact'; const mapFn = (children, fn, context) => { if (children == null) return null; return toChildArray(toChildArray(children).map(fn.bind(context))); }; // This API is completely unnecessary for Preact, so it's basically passthrough. export const Children = { map: mapFn, forEach: mapFn, count(children) { return children ? toChildArray(children).length : 0; }, only(children) { const normalized = toChildArray(children); if (normalized.length !== 1) throw 'Children.only'; return normalized[0]; }, toArray: toChildArray }; ================================================ FILE: compat/src/PureComponent.js ================================================ import { Component } from 'preact'; import { shallowDiffers } from './util'; /** * Component class with a predefined `shouldComponentUpdate` implementation */ export function PureComponent(p, c) { this.props = p; this.context = c; } PureComponent.prototype = new Component(); // Some third-party libraries check if this property is present PureComponent.prototype.isPureReactComponent = true; PureComponent.prototype.shouldComponentUpdate = function (props, state) { return shallowDiffers(this.props, props) || shallowDiffers(this.state, state); }; ================================================ FILE: compat/src/forwardRef.js ================================================ import { assign } from './util'; export const REACT_FORWARD_SYMBOL = Symbol.for('react.forward_ref'); /** * Pass ref down to a child. This is mainly used in libraries with HOCs that * wrap components. Using `forwardRef` there is an easy way to get a reference * of the wrapped component instead of one of the wrapper itself. * @param {import('./index').ForwardRefRenderFunction} fn * @returns {import('./internal').FunctionComponent} */ export function forwardRef(fn) { function Forwarded(props) { let clone = assign({}, props); delete clone.ref; return fn(clone, props.ref || null); } // mobx-react checks for this being present Forwarded.$$typeof = REACT_FORWARD_SYMBOL; // mobx-react heavily relies on implementation details. // It expects an object here with a `render` property, // and prototype.render will fail. Without this // mobx-react throws. Forwarded.render = fn; Forwarded.prototype.isReactComponent = true; Forwarded.displayName = 'ForwardRef(' + (fn.displayName || fn.name) + ')'; return Forwarded; } ================================================ FILE: compat/src/hooks.js ================================================ import { useState, useLayoutEffect, useEffect } from 'preact/hooks'; /** * This is taken from https://github.com/facebook/react/blob/main/packages/use-sync-external-store/src/useSyncExternalStoreShimClient.js#L84 * on a high level this cuts out the warnings, ... and attempts a smaller implementation * @typedef {{ _value: any; _getSnapshot: () => any }} Store */ export function useSyncExternalStore(subscribe, getSnapshot) { const value = getSnapshot(); /** * @typedef {{ _instance: Store }} StoreRef * @type {[StoreRef, (store: StoreRef) => void]} */ const [{ _instance }, forceUpdate] = useState({ _instance: { _value: value, _getSnapshot: getSnapshot } }); useLayoutEffect(() => { _instance._value = value; _instance._getSnapshot = getSnapshot; if (didSnapshotChange(_instance)) { forceUpdate({ _instance }); } }, [subscribe, value, getSnapshot]); useEffect(() => { if (didSnapshotChange(_instance)) { forceUpdate({ _instance }); } return subscribe(() => { if (didSnapshotChange(_instance)) { forceUpdate({ _instance }); } }); }, [subscribe]); return value; } /** @type {(inst: Store) => boolean} */ function didSnapshotChange(inst) { try { return !Object.is(inst._value, inst._getSnapshot()); } catch (error) { return true; } } export function startTransition(cb) { cb(); } export function useDeferredValue(val) { return val; } export function useTransition() { return [false, startTransition]; } // TODO: in theory this should be done after a VNode is diffed as we want to insert // styles/... before it attaches export const useInsertionEffect = useLayoutEffect; ================================================ FILE: compat/src/index.d.ts ================================================ import * as _preact from '../../src/index'; import { JSXInternal } from '../../src/jsx'; import * as _hooks from '../../hooks'; import * as _Suspense from './suspense'; declare namespace preact { export interface FunctionComponent

{ ( props: _preact.RenderableProps

, context?: any ): _preact.ComponentChildren; displayName?: string; defaultProps?: Partial

| undefined; } export interface ComponentClass

{ new (props: P, context?: any): _preact.Component; displayName?: string; defaultProps?: Partial

; contextType?: _preact.Context; getDerivedStateFromProps?( props: Readonly

, state: Readonly ): Partial | null; getDerivedStateFromError?(error: any): Partial | null; } // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging export interface Component

{ componentWillMount?(): void; componentDidMount?(): void; componentWillUnmount?(): void; getChildContext?(): object; componentWillReceiveProps?(nextProps: Readonly

, nextContext: any): void; shouldComponentUpdate?( nextProps: Readonly

, nextState: Readonly, nextContext: any ): boolean; componentWillUpdate?( nextProps: Readonly

, nextState: Readonly, nextContext: any ): void; getSnapshotBeforeUpdate?(oldProps: Readonly

, oldState: Readonly): any; componentDidUpdate?( previousProps: Readonly

, previousState: Readonly, snapshot: any ): void; componentDidCatch?(error: any, errorInfo: _preact.ErrorInfo): void; } export abstract class Component { constructor(props?: P, context?: any); static displayName?: string; static defaultProps?: any; static contextType?: _preact.Context; // Static members cannot reference class type parameters. This is not // supported in TypeScript. Reusing the same type arguments from `Component` // will lead to an impossible state where one cannot satisfy the type // constraint under no circumstances, see #1356.In general type arguments // seem to be a bit buggy and not supported well at the time of this // writing with TS 3.3.3333. static getDerivedStateFromProps?( props: Readonly, state: Readonly ): object | null; static getDerivedStateFromError?(error: any): object | null; state: Readonly; props: _preact.RenderableProps

; context: any; // From https://github.com/DefinitelyTyped/DefinitelyTyped/blob/e836acc75a78cf0655b5dfdbe81d69fdd4d8a252/types/react/index.d.ts#L402 // // We MUST keep setState() as a unified signature because it allows proper checking of the method return type. // // See: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18365#issuecomment-351013257 setState( state: | (( prevState: Readonly, props: Readonly

) => Pick | Partial | null) | (Pick | Partial | null), callback?: () => void ): void; forceUpdate(callback?: () => void): void; abstract render( props?: _preact.RenderableProps

, state?: Readonly, context?: any ): _preact.ComponentChildren; } } // export default React; export = React; export as namespace React; declare namespace React { // Export JSX export import JSX = JSXInternal; // Hooks export import CreateHandle = _hooks.CreateHandle; export import EffectCallback = _hooks.EffectCallback; export import Inputs = _hooks.Inputs; export import Reducer = _hooks.Reducer; export import Dispatch = _hooks.Dispatch; export import SetStateAction = _hooks.StateUpdater; export import useCallback = _hooks.useCallback; export import useContext = _hooks.useContext; export import useDebugValue = _hooks.useDebugValue; export import useEffect = _hooks.useEffect; export import useImperativeHandle = _hooks.useImperativeHandle; export import useId = _hooks.useId; export import useLayoutEffect = _hooks.useLayoutEffect; export import useMemo = _hooks.useMemo; export import useReducer = _hooks.useReducer; export import useRef = _hooks.useRef; export import useState = _hooks.useState; // React 18 hooks export import useInsertionEffect = _hooks.useLayoutEffect; export function useTransition(): [false, typeof startTransition]; export function useDeferredValue(val: T): T; export function useSyncExternalStore( subscribe: (flush: () => void) => () => void, getSnapshot: () => T ): T; // Preact Defaults export import Context = _preact.Context; export import ContextType = _preact.ContextType; export import RefObject = _preact.RefObject; export import Component = preact.Component; export import FunctionComponent = preact.FunctionComponent; export import ComponentType = _preact.ComponentType; export import ComponentClass = preact.ComponentClass; export import FC = _preact.FunctionComponent; export import createContext = _preact.createContext; export import Ref = _preact.Ref; export import createRef = _preact.createRef; export import Fragment = _preact.Fragment; export import createElement = _preact.createElement; export import cloneElement = _preact.cloneElement; export import ComponentProps = _preact.ComponentProps; export import ReactNode = _preact.ComponentChild; export import ReactElement = _preact.VNode; export import Consumer = _preact.Consumer; export import ErrorInfo = _preact.ErrorInfo; export import Key = _preact.Key; // Suspense export import Suspense = _Suspense.Suspense; export import lazy = _Suspense.lazy; // Compat export import StrictMode = _preact.Fragment; export const version: string; export function startTransition(cb: () => void): void; // HTML export interface HTMLAttributes extends _preact.HTMLAttributes {} export interface HTMLProps extends _preact.AllHTMLAttributes, _preact.ClassAttributes {} export interface AllHTMLAttributes extends _preact.AllHTMLAttributes {} export import DetailedHTMLProps = _preact.DetailedHTMLProps; export import CSSProperties = _preact.CSSProperties; export interface SVGProps extends _preact.SVGAttributes, _preact.ClassAttributes {} interface SVGAttributes extends _preact.SVGAttributes {} interface ReactSVG extends JSXInternal.IntrinsicSVGElements {} export import AriaAttributes = _preact.AriaAttributes; export import HTMLAttributeReferrerPolicy = _preact.HTMLAttributeReferrerPolicy; export import HTMLAttributeAnchorTarget = _preact.HTMLAttributeAnchorTarget; export import HTMLInputTypeAttribute = _preact.HTMLInputTypeAttribute; export import HTMLAttributeCrossOrigin = _preact.HTMLAttributeCrossOrigin; export import AnchorHTMLAttributes = _preact.AnchorHTMLAttributes; export import AudioHTMLAttributes = _preact.AudioHTMLAttributes; export import AreaHTMLAttributes = _preact.AreaHTMLAttributes; export import BaseHTMLAttributes = _preact.BaseHTMLAttributes; export import BlockquoteHTMLAttributes = _preact.BlockquoteHTMLAttributes; export import ButtonHTMLAttributes = _preact.ButtonHTMLAttributes; export import CanvasHTMLAttributes = _preact.CanvasHTMLAttributes; export import ColHTMLAttributes = _preact.ColHTMLAttributes; export import ColgroupHTMLAttributes = _preact.ColgroupHTMLAttributes; export import DataHTMLAttributes = _preact.DataHTMLAttributes; export import DetailsHTMLAttributes = _preact.DetailsHTMLAttributes; export import DelHTMLAttributes = _preact.DelHTMLAttributes; export import DialogHTMLAttributes = _preact.DialogHTMLAttributes; export import EmbedHTMLAttributes = _preact.EmbedHTMLAttributes; export import FieldsetHTMLAttributes = _preact.FieldsetHTMLAttributes; export import FormHTMLAttributes = _preact.FormHTMLAttributes; export import IframeHTMLAttributes = _preact.IframeHTMLAttributes; export import ImgHTMLAttributes = _preact.ImgHTMLAttributes; export import InsHTMLAttributes = _preact.InsHTMLAttributes; export import InputHTMLAttributes = _preact.InputHTMLAttributes; export import KeygenHTMLAttributes = _preact.KeygenHTMLAttributes; export import LabelHTMLAttributes = _preact.LabelHTMLAttributes; export import LiHTMLAttributes = _preact.LiHTMLAttributes; export import LinkHTMLAttributes = _preact.LinkHTMLAttributes; export import MapHTMLAttributes = _preact.MapHTMLAttributes; export import MenuHTMLAttributes = _preact.MenuHTMLAttributes; export import MediaHTMLAttributes = _preact.MediaHTMLAttributes; export import MetaHTMLAttributes = _preact.MetaHTMLAttributes; export import MeterHTMLAttributes = _preact.MeterHTMLAttributes; export import QuoteHTMLAttributes = _preact.QuoteHTMLAttributes; export import ObjectHTMLAttributes = _preact.ObjectHTMLAttributes; export import OlHTMLAttributes = _preact.OlHTMLAttributes; export import OptgroupHTMLAttributes = _preact.OptgroupHTMLAttributes; export import OptionHTMLAttributes = _preact.OptionHTMLAttributes; export import OutputHTMLAttributes = _preact.OutputHTMLAttributes; export import ParamHTMLAttributes = _preact.ParamHTMLAttributes; export import ProgressHTMLAttributes = _preact.ProgressHTMLAttributes; export import SlotHTMLAttributes = _preact.SlotHTMLAttributes; export import ScriptHTMLAttributes = _preact.ScriptHTMLAttributes; export import SelectHTMLAttributes = _preact.SelectHTMLAttributes; export import SourceHTMLAttributes = _preact.SourceHTMLAttributes; export import StyleHTMLAttributes = _preact.StyleHTMLAttributes; export import TableHTMLAttributes = _preact.TableHTMLAttributes; export import TextareaHTMLAttributes = _preact.TextareaHTMLAttributes; export import TdHTMLAttributes = _preact.TdHTMLAttributes; export import ThHTMLAttributes = _preact.ThHTMLAttributes; export import TimeHTMLAttributes = _preact.TimeHTMLAttributes; export import TrackHTMLAttributes = _preact.TrackHTMLAttributes; export import VideoHTMLAttributes = _preact.VideoHTMLAttributes; // Events export import TargetedEvent = _preact.TargetedEvent; export import ChangeEvent = _preact.TargetedEvent; export import ClipboardEvent = _preact.TargetedClipboardEvent; export import CompositionEvent = _preact.TargetedCompositionEvent; export import DragEvent = _preact.TargetedDragEvent; export import PointerEvent = _preact.TargetedPointerEvent; export import FocusEvent = _preact.TargetedFocusEvent; export import FormEvent = _preact.TargetedEvent; export import InvalidEvent = _preact.TargetedEvent; export import KeyboardEvent = _preact.TargetedKeyboardEvent; export import MouseEvent = _preact.TargetedMouseEvent; export import TouchEvent = _preact.TargetedTouchEvent; export import UIEvent = _preact.TargetedUIEvent; export import AnimationEvent = _preact.TargetedAnimationEvent; export import TransitionEvent = _preact.TargetedTransitionEvent; // Event Handler Types export import EventHandler = _preact.EventHandler; export import ChangeEventHandler = _preact.GenericEventHandler; export import ClipboardEventHandler = _preact.ClipboardEventHandler; export import CompositionEventHandler = _preact.CompositionEventHandler; export import DragEventHandler = _preact.DragEventHandler; export import PointerEventHandler = _preact.PointerEventHandler; export import FocusEventHandler = _preact.FocusEventHandler; export import FormEventHandler = _preact.GenericEventHandler; export import InvalidEventHandler = _preact.GenericEventHandler; export import KeyboardEventHandler = _preact.KeyboardEventHandler; export import MouseEventHandler = _preact.MouseEventHandler; export import TouchEventHandler = _preact.TouchEventHandler; export import UIEventHandler = _preact.UIEventHandler; export import AnimationEventHandler = _preact.AnimationEventHandler; export import TransitionEventHandler = _preact.TransitionEventHandler; export function createPortal( vnode: _preact.ComponentChildren, container: _preact.ContainerNode ): _preact.VNode; export function render( vnode: _preact.ComponentChild, parent: _preact.ContainerNode, callback?: () => void ): Component | null; export function hydrate( vnode: _preact.ComponentChild, parent: _preact.ContainerNode, callback?: () => void ): Component | null; export function unmountComponentAtNode( container: _preact.ContainerNode ): boolean; export function createFactory( type: _preact.VNode['type'] ): ( props?: any, ...children: _preact.ComponentChildren[] ) => _preact.VNode; export function isValidElement(element: any): boolean; export function isFragment(element: any): boolean; export function isMemo(element: any): boolean; export function findDOMNode( component: _preact.Component | Element ): Element | null; export abstract class PureComponent< P = {}, S = {}, SS = any > extends _preact.Component { isPureReactComponent: boolean; } export type MemoExoticComponent> = _preact.FunctionComponent> & { readonly type: C; }; export function memo

( component: _preact.FunctionalComponent

, comparer?: (prev: P, next: P) => boolean ): _preact.FunctionComponent

; export function memo>( component: C, comparer?: ( prev: _preact.ComponentProps, next: _preact.ComponentProps ) => boolean ): C; export interface RefAttributes extends _preact.Attributes { ref?: _preact.Ref | undefined; } export interface ForwardRefRenderFunction { (props: P, ref: ForwardedRef): _preact.ComponentChild; displayName?: string; } export interface ForwardRefExoticComponent

extends _preact.FunctionComponent

{ defaultProps?: Partial

| undefined; } export function forwardRef( fn: ForwardRefRenderFunction ): _preact.FunctionalComponent & { ref?: _preact.Ref }>; export type PropsWithoutRef

= Omit; interface MutableRefObject { current: T; } export type ForwardedRef = | ((instance: T | null) => void) | MutableRefObject | null; export type ElementType< P = any, Tag extends keyof JSX.IntrinsicElements = keyof JSX.IntrinsicElements > = | { [K in Tag]: P extends JSX.IntrinsicElements[K] ? K : never }[Tag] | ComponentType

; export type ComponentPropsWithoutRef = PropsWithoutRef< ComponentProps >; export type ComponentPropsWithRef = C extends new ( props: infer P ) => Component ? PropsWithoutRef

& RefAttributes> : ComponentProps; export type ElementRef< C extends | ForwardRefExoticComponent | { new (props: any): Component } | ((props: any) => ReactNode) | keyof JSXInternal.IntrinsicElements > = 'ref' extends keyof ComponentPropsWithRef ? NonNullable['ref']> extends RefAttributes< infer Instance >['ref'] ? Instance : never : never; export function flushSync(fn: () => R): R; export function flushSync(fn: (a: A) => R, a: A): R; export function unstable_batchedUpdates(callback: (a: A) => R, a: A): R; export function unstable_batchedUpdates(callback: () => R): R; export type PropsWithChildren

= P & { children?: _preact.ComponentChildren | undefined; }; export const Children: { map( children: T | T[], fn: (child: T, i: number) => R, context: any ): R[]; forEach( children: T | T[], fn: (child: T, i: number) => void, context: any ): void; count: (children: _preact.ComponentChildren) => number; only: (children: _preact.ComponentChildren) => _preact.ComponentChild; toArray: (children: _preact.ComponentChildren) => _preact.VNode<{}>[]; }; // scheduler export const unstable_ImmediatePriority: number; export const unstable_UserBlockingPriority: number; export const unstable_NormalPriority: number; export const unstable_LowPriority: number; export const unstable_IdlePriority: number; export function unstable_runWithPriority( priority: number, callback: () => void ): void; export const unstable_now: () => number; } ================================================ FILE: compat/src/index.js ================================================ import { Component, Fragment, createContext, createElement, createRef, options, cloneElement as preactCloneElement, render as preactRender } from 'preact'; import { useCallback, useContext, useDebugValue, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useReducer, useRef, useState } from 'preact/hooks'; import { Children } from './Children'; import { PureComponent } from './PureComponent'; import { forwardRef } from './forwardRef'; import { startTransition, useDeferredValue, useInsertionEffect, useSyncExternalStore, useTransition } from './hooks'; import { memo } from './memo'; import { createPortal } from './portals'; import { REACT_ELEMENT_TYPE, __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED, hydrate, render } from './render'; import { Suspense, lazy } from './suspense'; const version = '18.3.1'; // trick libraries to think we are react /** * Legacy version of createElement. * @param {import('./internal').VNode["type"]} type The node name or Component constructor */ function createFactory(type) { return createElement.bind(null, type); } /** * Check if the passed element is a valid (p)react node. * @param {*} element The element to check * @returns {boolean} */ function isValidElement(element) { return !!element && element.$$typeof === REACT_ELEMENT_TYPE; } /** * Check if the passed element is a Fragment node. * @param {*} element The element to check * @returns {boolean} */ function isFragment(element) { return isValidElement(element) && element.type === Fragment; } /** * Check if the passed element is a Memo node. * @param {*} element The element to check * @returns {boolean} */ function isMemo(element) { return ( !!element && typeof element.displayName == 'string' && element.displayName.indexOf('Memo(') == 0 ); } /** * Wrap `cloneElement` to abort if the passed element is not a valid element and apply * all vnode normalizations. * @param {import('./internal').VNode} element The vnode to clone * @param {object} props Props to add when cloning * @param {Array} rest Optional component children */ function cloneElement(element) { if (!isValidElement(element)) return element; return preactCloneElement.apply(null, arguments); } /** * Remove a component tree from the DOM, including state and event handlers. * @param {import('./internal').PreactElement} container * @returns {boolean} */ function unmountComponentAtNode(container) { if (container._children) { preactRender(null, container); return true; } return false; } /** * Get the matching DOM node for a component * @param {import('./internal').Component} component * @returns {import('./internal').PreactElement | null} */ function findDOMNode(component) { return ( (component && ((component._vnode && component._vnode._dom) || (component.nodeType === 1 && component))) || null ); } /** * In React, `flushSync` flushes the entire tree and forces a rerender. * @template Arg * @template Result * @param {(arg: Arg) => Result} callback function that runs before the flush * @param {Arg} [arg] Optional argument that can be passed to the callback * @returns */ const flushSync = (callback, arg) => { const prevDebounce = options.debounceRendering; options.debounceRendering = cb => cb(); const res = callback(arg); options.debounceRendering = prevDebounce; return res; }; /** * In React, `unstable_batchedUpdates` is a legacy feature that was made a no-op * outside of legacy mode in React 18 and a no-op across the board in React 19. * @template Arg * @template Result * @param {(arg: Arg) => Result} callback * @param {Arg} [arg] * @returns {Result} */ function unstable_batchedUpdates(callback, arg) { return callback(arg); } // compat to react-is export const isElement = isValidElement; export * from 'preact/hooks'; export { version, Children, render, hydrate, unmountComponentAtNode, createPortal, createElement, createContext, createFactory, cloneElement, createRef, Fragment, isValidElement, isFragment, isMemo, findDOMNode, Component, PureComponent, memo, forwardRef, flushSync, unstable_batchedUpdates, useInsertionEffect, startTransition, useDeferredValue, useSyncExternalStore, useTransition, Fragment as StrictMode, Suspense, lazy, __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED }; // React copies the named exports to the default one. export default { useState, useId, useReducer, useEffect, useLayoutEffect, useInsertionEffect, useTransition, useDeferredValue, useSyncExternalStore, startTransition, useRef, useImperativeHandle, useMemo, useCallback, useContext, useDebugValue, version, Children, render, hydrate, unmountComponentAtNode, createPortal, createElement, createContext, createFactory, cloneElement, createRef, Fragment, isValidElement, isElement, isFragment, isMemo, findDOMNode, Component, PureComponent, memo, forwardRef, flushSync, unstable_batchedUpdates, StrictMode: Fragment, Suspense, lazy, __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED }; ================================================ FILE: compat/src/internal.d.ts ================================================ import { Component as PreactComponent, VNode as PreactVNode, FunctionComponent as PreactFunctionComponent, PreactElement } from '../../src/internal'; import { SuspenseProps } from './suspense'; export { ComponentChildren } from '../..'; export { PreactElement }; export interface Component

extends PreactComponent { isReactComponent?: object; isPureReactComponent?: true; _patchedLifecycles?: true; // Suspense internal properties _childDidSuspend?(error: Promise, suspendingVNode: VNode): void; _suspended: (vnode: VNode) => (unsuspend: () => void) => void; _onResolve?(): void; // Portal internal properties _temp: any; _container: PreactElement; } export interface FunctionComponent

extends PreactFunctionComponent

{ shouldComponentUpdate?(nextProps: Readonly

): boolean; _patchedLifecycles?: true; } export interface VNode extends PreactVNode { $$typeof?: symbol; preactCompatNormalized?: boolean; } export interface SuspenseState { _suspended?: null | VNode; } export interface SuspenseComponent extends PreactComponent { _pendingSuspensionCount: number; _suspenders: Component[]; _detachOnNextRender: null | VNode; } ================================================ FILE: compat/src/memo.js ================================================ import { createElement } from 'preact'; import { shallowDiffers } from './util'; /** * Memoize a component, so that it only updates when the props actually have * changed. This was previously known as `React.pure`. * @param {import('./internal').FunctionComponent} c functional component * @param {(prev: object, next: object) => boolean} [comparer] Custom equality function * @returns {import('./internal').FunctionComponent} */ export function memo(c, comparer) { function shouldUpdate(nextProps) { const ref = this.props.ref; if (ref != nextProps.ref && ref) { typeof ref == 'function' ? ref(null) : (ref.current = null); } return comparer ? !comparer(this.props, nextProps) || ref != nextProps.ref : shallowDiffers(this.props, nextProps); } function Memoed(props) { this.shouldComponentUpdate = shouldUpdate; return createElement(c, props); } Memoed.displayName = 'Memo(' + (c.displayName || c.name) + ')'; Memoed._forwarded = Memoed.prototype.isReactComponent = true; Memoed.type = c; return Memoed; } ================================================ FILE: compat/src/portals.js ================================================ import { createElement, render } from 'preact'; /** * @param {import('../../src/index').RenderableProps<{ context: any }>} props */ function ContextProvider(props) { this.getChildContext = () => props.context; return props.children; } /** * Portal component * @this {import('./internal').Component} * @param {object | null | undefined} props * * TODO: use createRoot() instead of fake root */ function Portal(props) { const _this = this; let container = props._container; _this.componentWillUnmount = function () { render(null, _this._temp); _this._temp = null; _this._container = null; }; // When we change container we should clear our old container and // indicate a new mount. if (_this._container && _this._container !== container) { _this.componentWillUnmount(); } if (!_this._temp) { // Ensure the element has a mask for useId invocations let root = _this._vnode; while (root !== null && !root._mask && root._parent !== null) { root = root._parent; } _this._container = container; // Create a fake DOM parent node that manages a subset of `container`'s children: _this._temp = { nodeType: 1, parentNode: container, childNodes: [], _children: { _mask: root._mask }, ownerDocument: container.ownerDocument, namespaceURI: container.namespaceURI, insertBefore(child, before) { this.childNodes.push(child); _this._container.insertBefore(child, before); } }; } // Render our wrapping element into temp. render( createElement(ContextProvider, { context: _this.context }, props._vnode), _this._temp ); } /** * Create a `Portal` to continue rendering the vnode tree at a different DOM node * @param {import('./internal').VNode} vnode The vnode to render * @param {import('./internal').PreactElement} container The DOM node to continue rendering in to. */ export function createPortal(vnode, container) { const el = createElement(Portal, { _vnode: vnode, _container: container }); el.containerInfo = container; return el; } ================================================ FILE: compat/src/render.js ================================================ import { render as preactRender, hydrate as preactHydrate, options, toChildArray, Component } from 'preact'; import { useCallback, useContext, useDebugValue, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useReducer, useRef, useState } from 'preact/hooks'; import { useDeferredValue, useInsertionEffect, useSyncExternalStore, useTransition } from './index'; import { assign, IS_NON_DIMENSIONAL } from './util'; export const REACT_ELEMENT_TYPE = Symbol.for('react.element'); const CAMEL_PROPS = /^(?:accent|alignment|arabic|baseline|cap|clip(?!PathU)|color|dominant|fill|flood|font|glyph(?!R)|horiz|image(!S)|letter|lighting|marker(?!H|W|U)|overline|paint|pointer|shape|stop|strikethrough|stroke|text(?!L)|transform|underline|unicode|units|v|vector|vert|word|writing|x(?!C))[A-Z]/; const CAMEL_REPLACE = /[A-Z0-9]/g; const IS_DOM = typeof document !== 'undefined'; // Input types for which onchange should not be converted to oninput. const onChangeInputType = type => /fil|che|rad/.test(type); // Some libraries like `react-virtualized` explicitly check for this. Component.prototype.isReactComponent = true; // `UNSAFE_*` lifecycle hooks // Preact only ever invokes the unprefixed methods. // Here we provide a base "fallback" implementation that calls any defined UNSAFE_ prefixed method. // - If a component defines its own `componentDidMount()` (including via defineProperty), use that. // - If a component defines `UNSAFE_componentDidMount()`, `componentDidMount` is the alias getter/setter. // - If anything assigns to an `UNSAFE_*` property, the assignment is forwarded to the unprefixed property. // See https://github.com/preactjs/preact/issues/1941 [ 'componentWillMount', 'componentWillReceiveProps', 'componentWillUpdate' ].forEach(key => { Object.defineProperty(Component.prototype, key, { configurable: true, get() { return this['UNSAFE_' + key]; }, set(v) { Object.defineProperty(this, key, { configurable: true, writable: true, value: v }); } }); }); /** * Proxy render() since React returns a Component reference. * @param {import('./internal').VNode} vnode VNode tree to render * @param {import('./internal').PreactElement} parent DOM node to render vnode tree into * @param {() => void} [callback] Optional callback that will be called after rendering * @returns {import('./internal').Component | null} The root component reference or null */ export function render(vnode, parent, callback) { // React destroys any existing DOM nodes, see #1727 // ...but only on the first render, see #1828 if (parent._children == null) { parent.textContent = ''; } preactRender(vnode, parent); if (typeof callback == 'function') callback(); return vnode ? vnode._component : null; } export function hydrate(vnode, parent, callback) { preactHydrate(vnode, parent); if (typeof callback == 'function') callback(); return vnode ? vnode._component : null; } let oldEventHook = options.event; options.event = e => { if (oldEventHook) e = oldEventHook(e); e.persist = () => {}; e.isPropagationStopped = function isPropagationStopped() { return this.cancelBubble; }; e.isDefaultPrevented = function isDefaultPrevented() { return this.defaultPrevented; }; return (e.nativeEvent = e); }; const classNameDescriptorNonEnumberable = { configurable: true, get() { return this.class; } }; function handleDomVNode(vnode) { let props = vnode.props, type = vnode.type, normalizedProps = {}, isNonDashedType = type.indexOf('-') == -1; for (let i in props) { let value = props[i]; if ( (i === 'value' && 'defaultValue' in props && value == null) || // Emulate React's behavior of not rendering the contents of noscript tags on the client. (IS_DOM && i === 'children' && type === 'noscript') || i === 'class' || i === 'className' ) { // Skip applying value if it is null/undefined and we already set // a default value continue; } if (i === 'style' && typeof value === 'object') { for (let key in value) { if (typeof value[key] === 'number' && !IS_NON_DIMENSIONAL.test(key)) { value[key] += 'px'; } } } else if ( i === 'defaultValue' && 'value' in props && props.value == null ) { // `defaultValue` is treated as a fallback `value` when a value prop is present but null/undefined. // `defaultValue` for Elements with no value prop is the same as the DOM defaultValue property. i = 'value'; } else if (i === 'download' && value === true) { // Calling `setAttribute` with a truthy value will lead to it being // passed as a stringified value, e.g. `download="true"`. React // converts it to an empty string instead, otherwise the attribute // value will be used as the file name and the file will be called // "true" upon downloading it. value = ''; } else if (i === 'translate' && value === 'no') { value = false; } else if (i[0] === 'o' && i[1] === 'n') { let lowerCased = i.toLowerCase(); if (lowerCased === 'ondoubleclick') { i = 'ondblclick'; } else if ( lowerCased === 'onchange' && (type === 'input' || type === 'textarea') && !onChangeInputType(props.type) ) { lowerCased = i = 'oninput'; } else if (lowerCased === 'onfocus') { i = 'onfocusin'; } else if (lowerCased === 'onblur') { i = 'onfocusout'; } // Add support for onInput and onChange, see #3561 // if we have an oninput prop already change it to oninputCapture if (lowerCased === 'oninput') { i = lowerCased; if (normalizedProps[i]) { i = 'oninputCapture'; } } } else if (isNonDashedType && CAMEL_PROPS.test(i)) { i = i.replace(CAMEL_REPLACE, '-$&').toLowerCase(); } else if (value === null) { value = undefined; } normalizedProps[i] = value; } if (type == 'select') { // Add support for array select values: '); hydrate(, scratch); expect(scratch.firstElementChild.value).to.equal('foo'); expect(scratch.innerHTML).to.be.equal(''); }); it('should alias defaultValue to children', () => { render('); act(() => { set('hello'); }); // Note: This looks counterintuitive, but it's working correctly - the value // missing from HTML because innerHTML doesn't serialize form field values. // See demo: https://jsfiddle.net/4had2Lu8 // Related renderToString PR: preactjs/preact-render-to-string#161 expect(scratch.innerHTML).to.equal(''); expect(scratch.firstElementChild.value).to.equal('hello'); act(() => { set(''); }); expect(scratch.innerHTML).to.equal(''); expect(scratch.firstElementChild.value).to.equal(''); }); }); ================================================ FILE: compat/test/browser/unmountComponentAtNode.test.jsx ================================================ import React, { createElement, unmountComponentAtNode } from 'preact/compat'; import { setupScratch, teardown } from '../../../test/_util/helpers'; describe('unmountComponentAtNode', () => { /** @type {HTMLDivElement} */ let scratch; beforeEach(() => { scratch = setupScratch(); }); afterEach(() => { teardown(scratch); }); it('should unmount a root node', () => { const App = () =>

foo
; React.render(, scratch); expect(unmountComponentAtNode(scratch)).to.equal(true); expect(scratch.innerHTML).to.equal(''); }); it('should do nothing if root is not mounted', () => { expect(unmountComponentAtNode(scratch)).to.equal(false); expect(scratch.innerHTML).to.equal(''); }); }); ================================================ FILE: compat/test/browser/unstable_batchedUpdates.test.js ================================================ import { unstable_batchedUpdates } from 'preact/compat'; describe('unstable_batchedUpdates', () => { it('should execute & return cb', () => { expect(unstable_batchedUpdates(() => false)).to.equal(false); expect(unstable_batchedUpdates(arg => arg, true)).to.equal(true); }); }); ================================================ FILE: compat/test/browser/useSyncExternalStore.test.jsx ================================================ import React, { createElement, Fragment, useSyncExternalStore, render, useState, useCallback, useEffect, useLayoutEffect } from 'preact/compat'; import { setupRerender, act } from 'preact/test-utils'; import { setupScratch, teardown } from '../../../test/_util/helpers'; import { vi } from 'vitest'; const ReactDOM = React; describe('useSyncExternalStore', () => { /** @type {HTMLDivElement} */ let scratch; /** @type {() => void} */ let rerender; /** @type {{ logs: string[], log(arg: string): void; }} */ const Scheduler = { logs: [], log(arg) { this.logs.push(arg); } }; beforeEach(() => { scratch = setupScratch(); rerender = setupRerender(); }); afterEach(() => { teardown(scratch); Scheduler.logs = []; }); function defer(cb) { return Promise.resolve().then(cb); } function assertLog(expected) { expect(Scheduler.logs).to.deep.equal(expected); Scheduler.logs = []; } function Text({ text }) { Scheduler.log(text); return text; } /** @type {(container: Element) => { render(children: React.JSX.Element): void}} */ function createRoot(container) { return { render(children) { render(children, container); } }; } function createExternalStore(initialState) { const listeners = new Set(); let currentState = initialState; return { listeners, set(text) { currentState = text; listeners.forEach(listener => listener()); }, subscribe(listener) { listeners.add(listener); return () => listeners.delete(listener); }, getState() { return currentState; }, getSubscriberCount() { return listeners.size; } }; } it('subscribes and follows effects', () => { const subscribe = vi.fn(() => () => {}); const getSnapshot = vi.fn(() => 'hello world'); const App = () => { const value = useSyncExternalStore(subscribe, getSnapshot); return

{value}

; }; act(() => { render(, scratch); }); expect(scratch.innerHTML).to.equal('

hello world

'); expect(subscribe).toHaveBeenCalledOnce(); expect(getSnapshot).toHaveBeenCalledTimes(3); }); it('subscribes and rerenders when called', () => { /** @type {() => void} */ let flush; const subscribe = vi.fn(cb => { flush = cb; return () => {}; }); let called = false; const getSnapshot = vi.fn(() => { if (called) { return 'hello new world'; } return 'hello world'; }); const App = () => { const value = useSyncExternalStore(subscribe, getSnapshot); return

{value}

; }; act(() => { render(, scratch); }); expect(scratch.innerHTML).to.equal('

hello world

'); expect(subscribe).toHaveBeenCalledOnce(); expect(getSnapshot).toHaveBeenCalledTimes(3); called = true; flush(); rerender(); expect(scratch.innerHTML).to.equal('

hello new world

'); }); it('getSnapshot can return NaN without causing infinite loop', () => { /** @type {() => void} */ let flush; const subscribe = vi.fn(cb => { flush = cb; return () => {}; }); let called = false; const getSnapshot = vi.fn(() => { if (called) { return NaN; } return 1; }); const App = () => { const value = useSyncExternalStore(subscribe, getSnapshot); return

{value}

; }; act(() => { render(, scratch); }); expect(scratch.innerHTML).to.equal('

1

'); expect(subscribe).toHaveBeenCalledOnce(); expect(getSnapshot).toHaveBeenCalledTimes(3); called = true; flush(); rerender(); expect(scratch.innerHTML).to.equal('

NaN

'); }); it('should not call function values on subscription', () => { /** @type {() => void} */ let flush; const subscribe = vi.fn(cb => { flush = cb; return () => {}; }); const func = () => 'value: ' + i++; let i = 0; const getSnapshot = vi.fn(() => { return func; }); const App = () => { const value = useSyncExternalStore(subscribe, getSnapshot); return

{value()}

; }; act(() => { render(, scratch); }); expect(scratch.innerHTML).to.equal('

value: 0

'); expect(subscribe).toHaveBeenCalledOnce(); expect(getSnapshot).toHaveBeenCalledTimes(3); flush(); rerender(); expect(scratch.innerHTML).to.equal('

value: 0

'); }); it('should work with changing getSnapshot', () => { /** @type {() => void} */ let flush; const subscribe = vi.fn(cb => { flush = cb; return () => {}; }); let i = 0; const App = () => { const value = useSyncExternalStore(subscribe, () => { return i; }); return

value: {value}

; }; act(() => { render(, scratch); }); expect(scratch.innerHTML).to.equal('

value: 0

'); expect(subscribe).toHaveBeenCalledOnce(); i++; flush(); rerender(); expect(scratch.innerHTML).to.equal('

value: 1

'); }); it('works with useCallback', () => { /** @type {() => void} */ let toggle; const App = () => { const [state, setState] = useState(true); toggle = setState.bind(this, () => false); const value = useSyncExternalStore( useCallback(() => { return () => {}; }, [state]), () => (state ? 'yep' : 'nope') ); return

{value}

; }; act(() => { render(, scratch); }); expect(scratch.innerHTML).to.equal('

yep

'); toggle(); rerender(); expect(scratch.innerHTML).to.equal('

nope

'); }); it('handles store updates before subscribing', async () => { // This test is testing scheduling mechanics, so teardown the manual // rerender test setup to rely on Preact's built-in scheduling and verify // this behavior works. We still need a DOM container to render into so set // that back up. teardown(scratch); scratch = setupScratch(); const store = createExternalStore(0); function App() { const value = useSyncExternalStore(store.subscribe, store.getState); useEffect(() => { Scheduler.log('Passive effect: ' + value); }, [value]); return ; } const container = document.createElement('div'); const root = createRoot(container); // Schedule a mutation in the next microtask after the initial render but // before subscribing to the store const mutation = defer(() => { // Assert we are running this mutation before subscribing to the store expect(store.listeners.size).to.equal(0); store.set(1); }); root.render(); expect(container.textContent).to.equal('0'); assertLog([0]); // Wait for the mutation to occur. Then wait for the passive effects that // subscribe to the store and log the new value. await mutation; await new Promise(r => setTimeout(r, 32)); expect(container.textContent).to.equal('1'); expect(store.listeners.size).to.equal(1); assertLog(['Passive effect: 0', 1, 'Passive effect: 1']); }); // The following tests are taken from the React test suite: // https://github.com/facebook/react/blob/3e09c27b880e1fecdb1eca5db510ecce37ea6be2/packages/use-sync-external-store/src/__tests__/useSyncExternalStoreShared-test.js describe('React useSyncExternalStore test suite', () => { it('basic usage', async () => { const store = createExternalStore('Initial'); function App() { const text = useSyncExternalStore(store.subscribe, store.getState); return ; } const container = document.createElement('div'); const root = createRoot(container); await act(() => root.render()); assertLog(['Initial']); expect(container.textContent).to.equal('Initial'); await act(() => { store.set('Updated'); }); assertLog(['Updated']); expect(container.textContent).to.equal('Updated'); }); it('skips re-rendering if nothing changes', async () => { const store = createExternalStore('Initial'); function App() { const text = useSyncExternalStore(store.subscribe, store.getState); return ; } const container = document.createElement('div'); const root = createRoot(container); await act(() => root.render()); assertLog(['Initial']); expect(container.textContent).to.equal('Initial'); // Update to the same value await act(() => { store.set('Initial'); }); // Should not re-render assertLog([]); expect(container.textContent).to.equal('Initial'); }); it('switch to a different store', async () => { const storeA = createExternalStore(0); const storeB = createExternalStore(0); let setStore; function App() { const [store, _setStore] = useState(storeA); setStore = _setStore; const value = useSyncExternalStore(store.subscribe, store.getState); return ; } const container = document.createElement('div'); const root = createRoot(container); await act(() => root.render()); assertLog([0]); expect(container.textContent).to.equal('0'); await act(() => { storeA.set(1); }); assertLog([1]); expect(container.textContent).to.equal('1'); // Switch stores and update in the same batch await act(() => { // This update will be disregarded storeA.set(2); setStore(storeB); }); // Now reading from B instead of A assertLog([0]); expect(container.textContent).to.equal('0'); // Update A await act(() => { storeA.set(3); }); // Nothing happened, because we're no longer subscribed to A assertLog([]); expect(container.textContent).to.equal('0'); // Update B await act(() => { storeB.set(1); }); assertLog([1]); expect(container.textContent).to.equal('1'); }); it('selecting a specific value inside getSnapshot', async () => { const store = createExternalStore({ a: 0, b: 0 }); function A() { const a = useSyncExternalStore( store.subscribe, () => store.getState().a ); return ; } function B() { const b = useSyncExternalStore( store.subscribe, () => store.getState().b ); return ; } function App() { return ( ); } const container = document.createElement('div'); const root = createRoot(container); await act(() => root.render()); assertLog(['A0', 'B0']); expect(container.textContent).to.equal('A0B0'); // Update b but not a await act(() => { store.set({ a: 0, b: 1 }); }); // Only b re-renders assertLog(['B1']); expect(container.textContent).to.equal('A0B1'); // Update a but not b await act(() => { store.set({ a: 1, b: 1 }); }); // Only a re-renders assertLog(['A1']); expect(container.textContent).to.equal('A1B1'); }); // In React 18, you can't observe in between a sync render and its // passive effects, so this is only relevant to legacy roots // @gate enableUseSyncExternalStoreShim it("compares to current state before bailing out, even when there's a mutation in between the sync and passive effects", async () => { const store = createExternalStore(0); function App() { const value = useSyncExternalStore(store.subscribe, store.getState); useEffect(() => { Scheduler.log('Passive effect: ' + value); }, [value]); return ; } const container = document.createElement('div'); const root = createRoot(container); await act(() => root.render()); assertLog([0, 'Passive effect: 0']); // Schedule an update. We'll intentionally not use `act` so that we can // insert a mutation before React subscribes to the store in a // passive effect. store.set(1); rerender(); assertLog([ 1 // Passive effect hasn't fired yet ]); expect(container.textContent).to.equal('1'); // Flip the store state back to the previous value. store.set(0); rerender(); assertLog([ 'Passive effect: 1', // Re-render. If the current state were tracked by updating a ref in a // passive effect, then this would break because the previous render's // passive effect hasn't fired yet, so we'd incorrectly think that // the state hasn't changed. 0 ]); // Should flip back to 0 expect(container.textContent).to.equal('0'); // Preact: Wait for 'Passive effect: 0' to flush from the rAF so it doesn't impact other tests await new Promise(r => setTimeout(r, 32)); }); it('mutating the store in between render and commit when getSnapshot has changed', async () => { const store = createExternalStore({ a: 1, b: 1 }); const getSnapshotA = () => store.getState().a; const getSnapshotB = () => store.getState().b; function Child1({ step }) { const value = useSyncExternalStore(store.subscribe, store.getState); useLayoutEffect(() => { if (step === 1) { // Update B in a layout effect. This happens in the same commit // that changed the getSnapshot in Child2. Child2's effects haven't // fired yet, so it doesn't have access to the latest getSnapshot. So // it can't use the getSnapshot to bail out. Scheduler.log('Update B in commit phase'); store.set({ a: value.a, b: 2 }); } }, [step]); return null; } function Child2({ step }) { const label = step === 0 ? 'A' : 'B'; const getSnapshot = step === 0 ? getSnapshotA : getSnapshotB; const value = useSyncExternalStore(store.subscribe, getSnapshot); return ; } let setStep; function App() { const [step, _setStep] = useState(0); setStep = _setStep; return ( <> ); } const container = document.createElement('div'); const root = createRoot(container); await act(() => root.render()); assertLog(['A1']); expect(container.textContent).to.equal('A1'); await act(() => { // Change getSnapshot and update the store in the same batch setStep(1); }); assertLog([ 'B1', 'Update B in commit phase', // If Child2 had used the old getSnapshot to bail out, then it would have // incorrectly bailed out here instead of re-rendering. 'B2' ]); expect(container.textContent).to.equal('B2'); }); it('mutating the store in between render and commit when getSnapshot has _not_ changed', async () => { // Same as previous test, but `getSnapshot` does not change const store = createExternalStore({ a: 1, b: 1 }); const getSnapshotA = () => store.getState().a; function Child1({ step }) { const value = useSyncExternalStore(store.subscribe, store.getState); useLayoutEffect(() => { if (step === 1) { // Update B in a layout effect. This happens in the same commit // that changed the getSnapshot in Child2. Child2's effects haven't // fired yet, so it doesn't have access to the latest getSnapshot. So // it can't use the getSnapshot to bail out. Scheduler.log('Update B in commit phase'); store.set({ a: value.a, b: 2 }); } }, [step]); return null; } function Child2({ step }) { const value = useSyncExternalStore(store.subscribe, getSnapshotA); return ; } let setStep; function App() { const [step, _setStep] = useState(0); setStep = _setStep; return ( <> ); } const container = document.createElement('div'); const root = createRoot(container); await act(() => root.render()); assertLog(['A1']); expect(container.textContent).to.equal('A1'); // This will cause a layout effect, and in the layout effect we'll update // the store await act(() => { setStep(1); }); assertLog([ 'A1', // This updates B, but since Child2 doesn't subscribe to B, it doesn't // need to re-render. 'Update B in commit phase' // No re-render ]); expect(container.textContent).to.equal('A1'); }); it("does not bail out if the previous update hasn't finished yet", async () => { const store = createExternalStore(0); function Child1() { const value = useSyncExternalStore(store.subscribe, store.getState); useLayoutEffect(() => { if (value === 1) { Scheduler.log('Reset back to 0'); store.set(0); } }, [value]); return ; } function Child2() { const value = useSyncExternalStore(store.subscribe, store.getState); return ; } const container = document.createElement('div'); const root = createRoot(container); await act(() => root.render( <> ) ); assertLog([0, 0]); expect(container.textContent).to.equal('00'); await act(() => { store.set(1); }); // Preact logs differ from React here cuz of how we do rerendering. We // rerender subtrees and then commit effects so Child2 never sees the // update to 1 cuz Child1 rerenders and runs its layout effects first. assertLog([1, /*1,*/ 'Reset back to 0', 0, 0]); expect(container.textContent).to.equal('00'); }); it('uses the latest getSnapshot, even if it changed in the same batch as a store update', async () => { const store = createExternalStore({ a: 0, b: 0 }); const getSnapshotA = () => store.getState().a; const getSnapshotB = () => store.getState().b; let setGetSnapshot; function App() { const [getSnapshot, _setGetSnapshot] = useState(() => getSnapshotA); setGetSnapshot = _setGetSnapshot; const text = useSyncExternalStore(store.subscribe, getSnapshot); return ; } const container = document.createElement('div'); const root = createRoot(container); await act(() => root.render()); assertLog([0]); // Update the store and getSnapshot at the same time await act(() => { setGetSnapshot(() => getSnapshotB); store.set({ a: 1, b: 2 }); }); // It should read from B instead of A assertLog([2]); expect(container.textContent).to.equal('2'); }); it('handles errors thrown by getSnapshot', async () => { class ErrorBoundary extends React.Component { state = { error: null }; static getDerivedStateFromError(error) { return { error }; } render() { if (this.state.error) { return ; } return this.props.children; } } const store = createExternalStore({ value: 0, throwInGetSnapshot: false, throwInIsEqual: false }); function App() { const { value } = useSyncExternalStore(store.subscribe, () => { const state = store.getState(); if (state.throwInGetSnapshot) { throw new Error('Error in getSnapshot'); } return state; }); return ; } const errorBoundary = React.createRef(); const container = document.createElement('div'); const root = createRoot(container); await act(() => root.render( ) ); assertLog([0]); expect(container.textContent).to.equal('0'); // Update that throws in a getSnapshot. We can catch it with an error boundary. await act(() => { store.set({ value: 1, throwInGetSnapshot: true, throwInIsEqual: false }); }); assertLog(['Error in getSnapshot']); expect(container.textContent).to.equal('Error in getSnapshot'); }); it('getSnapshot can return NaN without infinite loop warning', async () => { const store = createExternalStore('not a number'); function App() { const value = useSyncExternalStore(store.subscribe, () => parseInt(store.getState(), 10) ); return ; } const container = document.createElement('div'); const root = createRoot(container); // Initial render that reads a snapshot of NaN. This is OK because we use // Object.is algorithm to compare values. await act(() => root.render()); expect(container.textContent).to.equal('NaN'); // Update to real number await act(() => store.set(123)); expect(container.textContent).to.equal('123'); // Update back to NaN await act(() => store.set('not a number')); expect(container.textContent).to.equal('NaN'); }); it('regression test for facebook/react#23150', async () => { const store = createExternalStore('Initial'); function App() { const text = useSyncExternalStore(store.subscribe, store.getState); const [derivedText, setDerivedText] = useState(text); useEffect(() => {}, []); if (derivedText !== text.toUpperCase()) { setDerivedText(text.toUpperCase()); } return ; } const container = document.createElement('div'); const root = createRoot(container); await act(() => root.render()); assertLog(['INITIAL']); expect(container.textContent).to.equal('INITIAL'); await act(() => { store.set('Updated'); }); assertLog(['UPDATED']); expect(container.textContent).to.equal('UPDATED'); }); }); }); ================================================ FILE: compat/test/ts/forward-ref.tsx ================================================ import React from '../../src'; const MyInput: React.ForwardRefRenderFunction< { focus(): void }, { id: string } > = (props, ref) => { const inputRef = React.useRef(null); React.useImperativeHandle(ref, () => ({ focus: () => { if (inputRef.current) { inputRef.current.focus(); } } })); return ; }; export const foo = React.forwardRef(MyInput); export const Bar = React.forwardRef( (props, ref) => { return
{props.children}
; } ); export const baz = ( ref: React.ForwardedRef ): React.Ref => ref; ================================================ FILE: compat/test/ts/index.tsx ================================================ import React from '../../src'; React.render(
, document.createElement('div')); React.render(
, document.createDocumentFragment()); React.render(
, document.body.shadowRoot!); React.hydrate(
, document.createElement('div')); React.hydrate(
, document.createDocumentFragment()); React.hydrate(
, document.body.shadowRoot!); React.unmountComponentAtNode(document.createElement('div')); React.unmountComponentAtNode(document.createDocumentFragment()); React.unmountComponentAtNode(document.body.shadowRoot!); React.createPortal(
, document.createElement('div')); React.createPortal(
, document.createDocumentFragment()); React.createPortal(
, document.body.shadowRoot!); const Ctx = React.createContext({ contextValue: '' }); class SimpleComponentWithContextAsProvider extends React.Component { componentProp = 'componentProp'; render() { // Render inside div to ensure standard JSX elements still work return (
{/* Ensure context still works */} {({ contextValue }) => contextValue.toLowerCase()}
); } } SimpleComponentWithContextAsProvider.defaultProps = { foo: 'default' }; React.render( , document.createElement('div') ); ================================================ FILE: compat/test/ts/lazy.tsx ================================================ import * as React from '../../src'; export interface LazyProps { isProp: boolean; } interface LazyState { forState: string; } export default class IsLazyComponent extends React.Component< LazyProps, LazyState > { render({ isProp }: LazyProps) { return
{isProp ? 'Super Lazy TRUE' : 'Super Lazy FALSE'}
; } } ================================================ FILE: compat/test/ts/memo.tsx ================================================ import * as React from '../../src'; import { expectType } from './utils'; interface MemoProps { required: string; optional?: string; defaulted: string; } interface MemoPropsExceptDefaults { required: string; optional?: string; } const ComponentExceptDefaults = () =>
; const ReadonlyBaseComponent = (props: Readonly) => (
{props.required + props.optional + props.defaulted}
); ReadonlyBaseComponent.defaultProps = { defaulted: '' }; const BaseComponent = (props: MemoProps) => (
{props.required + props.optional + props.defaulted}
); BaseComponent.defaultProps = { defaulted: '' }; // memo for readonly component with default comparison const MemoedReadonlyComponent = React.memo(ReadonlyBaseComponent); expectType>(MemoedReadonlyComponent); export const memoedReadonlyComponent = ( ); // memo for non-readonly component with default comparison const MemoedComponent = React.memo(BaseComponent); expectType>(MemoedComponent); export const memoedComponent = ; // memo with custom comparison const CustomMemoedComponent = React.memo(BaseComponent, (a, b) => { expectType(a); expectType(b); return a.required === b.required; }); expectType>(CustomMemoedComponent); export const customMemoedComponent = ; const MemoedComponentExceptDefaults = React.memo( ComponentExceptDefaults ); expectType>( MemoedComponentExceptDefaults ); export const memoedComponentExceptDefaults = ( ); ================================================ FILE: compat/test/ts/react-default.tsx ================================================ import React from '../../src'; class ReactIsh extends React.Component { render() { return
Text
; } } ================================================ FILE: compat/test/ts/react-star.tsx ================================================ // import React from '../../src'; import * as React from '../../src'; class ReactIsh extends React.Component { render() { return
Text
; } } ================================================ FILE: compat/test/ts/scheduler.ts ================================================ import { unstable_runWithPriority, unstable_NormalPriority, unstable_LowPriority, unstable_IdlePriority, unstable_UserBlockingPriority, unstable_ImmediatePriority, unstable_now } from '../../src'; const noop = () => null; unstable_runWithPriority(unstable_IdlePriority, noop); unstable_runWithPriority(unstable_LowPriority, noop); unstable_runWithPriority(unstable_NormalPriority, noop); unstable_runWithPriority(unstable_UserBlockingPriority, noop); unstable_runWithPriority(unstable_ImmediatePriority, noop); if (typeof unstable_now() === 'number') { } ================================================ FILE: compat/test/ts/suspense.tsx ================================================ import * as React from '../../src'; interface LazyProps { isProp: boolean; } const IsLazyFunctional = (props: LazyProps) => (
{props.isProp ? 'Super Lazy TRUE' : 'Super Lazy FALSE'}
); const FallBack = () =>
Still working...
; /** * Have to mock dynamic import as import() throws a syntax error in the test runner */ const componentPromise = new Promise<{ default: typeof IsLazyFunctional }>( resolve => { setTimeout(() => { resolve({ default: IsLazyFunctional }); }, 800); } ); /** * For usage with import: * const IsLazyComp = lazy(() => import('./lazy')); */ const IsLazyFunc = React.lazy(() => componentPromise); // Suspense using lazy component class ReactSuspensefulFunc extends React.Component { render() { return ( }> ); } } const Comp = () =>

Hello world

; const importComponent = async () => { return { MyComponent: Comp }; }; const Lazy = React.lazy(() => importComponent().then(mod => ({ default: mod.MyComponent })) ); // eslint-disable-next-line function App() { return ; } ================================================ FILE: compat/test/ts/tsconfig.json ================================================ { "compilerOptions": { "target": "es6", "module": "es6", "moduleResolution": "node", "lib": ["es6", "dom"], "strict": true, "forceConsistentCasingInFileNames": true, "jsx": "react", "noEmit": true, "allowSyntheticDefaultImports": true, "paths": { "preact": ["../../../src/index.d.ts"] } }, "include": ["./**/*.ts", "./**/*.tsx"] } ================================================ FILE: compat/test/ts/utils.ts ================================================ /** * Assert the parameter is of a specific type. */ export const expectType = (_: T): void => undefined; ================================================ FILE: compat/test-utils.js ================================================ module.exports = require('preact/test-utils'); ================================================ FILE: compat/test-utils.mjs ================================================ export * from 'preact/test-utils'; ================================================ FILE: config/compat-entries.js ================================================ const path = require('path'); const fs = require('fs'); const kl = require('kolorist'); const pkgFiles = new Set(require('../package.json').files); const compatDir = path.join(__dirname, '..', 'compat'); const files = fs.readdirSync(compatDir); let missing = 0; for (const file of files) { const expected = 'compat/' + file; if (/\.(js|mjs)$/.test(file) && !pkgFiles.has(expected)) { missing++; const filePath = kl.cyan('compat/' + file); const label = kl.inverse(kl.red(' ERROR ')); console.error( `${label} File ${filePath} is missing in "files" entry in package.json` ); } } if (missing > 0) { process.exit(1); } ================================================ FILE: debug/LICENSE ================================================ The MIT License (MIT) Copyright (c) 2015-present Jason Miller Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: debug/mangle.json ================================================ { "help": { "what is this file?": "It controls protected/private property mangling so that minified builds have consistent property names.", "why are there duplicate minified properties?": "Most properties are only used on one type of objects, so they can have the same name since they will never collide. Doing this reduces size." }, "minify": { "mangle": { "properties": { "regex": "^_[^_]", "reserved": [ "__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED", "__REACT_DEVTOOLS_GLOBAL_HOOK__", "__PREACT_DEVTOOLS__", "_renderers", "__source", "__self" ] } } } } ================================================ FILE: debug/package.json ================================================ { "name": "preact-debug", "amdName": "preactDebug", "private": true, "description": "Preact extensions for development", "main": "dist/debug.js", "module": "dist/debug.mjs", "umd:main": "dist/debug.umd.js", "source": "src/index.js", "types": "src/index.d.ts", "license": "MIT", "mangle": { "regex": "^(?!_renderer)^_" }, "peerDependencies": { "preact": "^10.0.0" } } ================================================ FILE: debug/src/check-props.js ================================================ const ReactPropTypesSecret = 'SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED'; let loggedTypeFailures = {}; /** * Reset the history of which prop type warnings have been logged. */ export function resetPropWarnings() { loggedTypeFailures = {}; } /** * Assert that the values match with the type specs. * Error messages are memorized and will only be shown once. * * Adapted from https://github.com/facebook/prop-types/blob/master/checkPropTypes.js * * @param {object} typeSpecs Map of name to a ReactPropType * @param {object} values Runtime values that need to be type-checked * @param {string} location e.g. "prop", "context", "child context" * @param {string} componentName Name of the component for error messages. * @param {?Function} getStack Returns the component stack. */ export function checkPropTypes( typeSpecs, values, location, componentName, getStack ) { Object.keys(typeSpecs).forEach(typeSpecName => { let error; try { error = typeSpecs[typeSpecName]( values, typeSpecName, componentName, location, null, ReactPropTypesSecret ); } catch (e) { error = e; } if (error && !(error.message in loggedTypeFailures)) { loggedTypeFailures[error.message] = true; console.error( `Failed ${location} type: ${error.message}${ (getStack && `\n${getStack()}`) || '' }` ); } }); } ================================================ FILE: debug/src/component-stack.js ================================================ import { options, Fragment } from 'preact'; /** * Get human readable name of the component/dom node * * @param {import('./internal').VNode} vnode * @returns {string} */ export function getDisplayName(vnode) { if (vnode.type === Fragment) { return 'Fragment'; } else if (typeof vnode.type == 'function') { return vnode.type.displayName || vnode.type.name; } else if (typeof vnode.type == 'string') { return vnode.type; } return '#text'; } /** * Used to keep track of the currently rendered `vnode` and print it * in debug messages. */ let renderStack = []; /** * Keep track of the current owners. An owner describes a component * which was responsible to render a specific `vnode`. This exclude * children that are passed via `props.children`, because they belong * to the parent owner. * * ```jsx * const Foo = props =>
{props.children}
// div's owner is Foo * const Bar = props => { * return ( * // Foo's owner is Bar, span's owner is Bar * ) * } * ``` * * Note: A `vnode` may be hoisted to the root scope due to compiler * optimiztions. In these cases the owner will be different. */ let ownerStack = []; const ownerMap = new WeakMap(); /** * Get the currently rendered `vnode` * @returns {import('./internal').VNode | null} */ export function getCurrentVNode() { return renderStack.length > 0 ? renderStack[renderStack.length - 1] : null; } /** * If the user doesn't have `@babel/plugin-transform-react-jsx-source` * somewhere in his tool chain we can't print the filename and source * location of a component. In that case we just omit that, but we'll * print a helpful message to the console, notifying the user of it. */ let showJsxSourcePluginWarning = true; /** * Check if a `vnode` is a possible owner. * @param {import('./internal').VNode} vnode */ function isPossibleOwner(vnode) { return typeof vnode.type == 'function' && vnode.type != Fragment; } /** * Return the component stack that was captured up to this point. * @param {import('./internal').VNode} vnode * @returns {string} */ export function getOwnerStack(vnode) { const stack = [vnode]; let next = vnode; while ((next = ownerMap.get(next)) != null) { stack.push(next); } return stack.reduce((acc, owner) => { acc += ` in ${getDisplayName(owner)}`; const source = owner.__source; if (source) { acc += ` (at ${source.fileName}:${source.lineNumber})`; } else if (showJsxSourcePluginWarning) { console.warn( 'Add @babel/plugin-transform-react-jsx-source to get a more detailed component stack. Note that you should not add it to production builds of your App for bundle size reasons.' ); } showJsxSourcePluginWarning = false; return (acc += '\n'); }, ''); } /** * Setup code to capture the component trace while rendering. Note that * we cannot simply traverse `vnode._parent` upwards, because we have some * debug messages for `this.setState` where the `vnode` is `undefined`. */ export function setupComponentStack() { let oldDiff = options._diff; let oldDiffed = options.diffed; let oldRoot = options._root; let oldVNode = options.vnode; let oldRender = options._render; options.diffed = vnode => { if (isPossibleOwner(vnode)) { ownerStack.pop(); } renderStack.pop(); if (oldDiffed) oldDiffed(vnode); }; options._diff = vnode => { if (isPossibleOwner(vnode)) { renderStack.push(vnode); } if (oldDiff) oldDiff(vnode); }; options._root = (vnode, parent) => { ownerStack = []; if (oldRoot) oldRoot(vnode, parent); }; options.vnode = vnode => { ownerMap.set( vnode, ownerStack.length > 0 ? ownerStack[ownerStack.length - 1] : null ); if (oldVNode) oldVNode(vnode); }; options._render = vnode => { if (isPossibleOwner(vnode)) { ownerStack.push(vnode); } if (oldRender) oldRender(vnode); }; } /** * Return the component stack that was captured up to this point. * @returns {string} */ export function captureOwnerStack() { return getOwnerStack(getCurrentVNode()); } ================================================ FILE: debug/src/constants.js ================================================ export const ELEMENT_NODE = 1; export const DOCUMENT_NODE = 9; export const DOCUMENT_FRAGMENT_NODE = 11; ================================================ FILE: debug/src/debug.js ================================================ import { checkPropTypes } from './check-props'; import { options, Component } from 'preact'; import { ELEMENT_NODE, DOCUMENT_NODE, DOCUMENT_FRAGMENT_NODE } from './constants'; import { getOwnerStack, setupComponentStack, getCurrentVNode, getDisplayName } from './component-stack'; import { isNaN } from './util'; const isWeakMapSupported = typeof WeakMap == 'function'; /** * @param {import('./internal').VNode} vnode * @returns {Array} */ function getDomChildren(vnode) { let domChildren = []; if (!vnode._children) return domChildren; vnode._children.forEach(child => { if (child && typeof child.type === 'function') { domChildren.push.apply(domChildren, getDomChildren(child)); } else if (child && typeof child.type === 'string') { domChildren.push(child.type); } }); return domChildren; } /** * @param {import('./internal').VNode} parent * @returns {string} */ function getClosestDomNodeParentName(parent) { if (!parent) return ''; if (typeof parent.type == 'function') { if (parent._parent == null) { if (parent._dom != null && parent._dom.parentNode != null) { return parent._dom.parentNode.localName; } return ''; } return getClosestDomNodeParentName(parent._parent); } return /** @type {string} */ (parent.type); } export function initDebug() { setupComponentStack(); let hooksAllowed = false; /* eslint-disable no-console */ let oldBeforeDiff = options._diff; let oldDiffed = options.diffed; let oldVnode = options.vnode; let oldRender = options._render; let oldCatchError = options._catchError; let oldRoot = options._root; let oldHook = options._hook; const warnedComponents = !isWeakMapSupported ? null : { useEffect: new WeakMap(), useLayoutEffect: new WeakMap(), lazyPropTypes: new WeakMap() }; const deprecations = []; options._catchError = (error, vnode, oldVNode, errorInfo) => { let component = vnode && vnode._component; if (component && typeof error.then == 'function') { const promise = error; error = new Error( `Missing Suspense. The throwing component was: ${getDisplayName(vnode)}` ); let parent = vnode; for (; parent; parent = parent._parent) { if (parent._component && parent._component._childDidSuspend) { error = promise; break; } } // We haven't recovered and we know at this point that there is no // Suspense component higher up in the tree if (error instanceof Error) { throw error; } } try { errorInfo = errorInfo || {}; errorInfo.componentStack = getOwnerStack(vnode); oldCatchError(error, vnode, oldVNode, errorInfo); // when an error was handled by an ErrorBoundary we will nonetheless emit an error // event on the window object. This is to make up for react compatibility in dev mode // and thus make the Next.js dev overlay work. if (typeof error.then != 'function') { setTimeout(() => { throw error; }); } } catch (e) { throw e; } }; options._root = (vnode, parentNode) => { if (!parentNode) { throw new Error( 'Undefined parent passed to render(), this is the second argument.\n' + 'Check if the element is available in the DOM/has the correct id.' ); } let isValid; switch (parentNode.nodeType) { case ELEMENT_NODE: case DOCUMENT_FRAGMENT_NODE: case DOCUMENT_NODE: isValid = true; break; default: isValid = false; } if (!isValid) { let componentName = getDisplayName(vnode); throw new Error( `Expected a valid HTML node as a second argument to render. Received ${parentNode} instead: render(<${componentName} />, ${parentNode});` ); } if (oldRoot) oldRoot(vnode, parentNode); }; options._diff = vnode => { let { type } = vnode; hooksAllowed = true; if (type === undefined) { throw new Error( 'Undefined component passed to createElement()\n\n' + 'You likely forgot to export your component or might have mixed up default and named imports' + serializeVNode(vnode) + `\n\n${getOwnerStack(vnode)}` ); } else if (type != null && typeof type == 'object') { if (type._children !== undefined && type._dom !== undefined) { throw new Error( `Invalid type passed to createElement(): ${type}\n\n` + 'Did you accidentally pass a JSX literal as JSX twice?\n\n' + ` let My${getDisplayName(vnode)} = ${serializeVNode(type)};\n` + ` let vnode = ;\n\n` + 'This usually happens when you export a JSX literal and not the component.' + `\n\n${getOwnerStack(vnode)}` ); } throw new Error( 'Invalid type passed to createElement(): ' + (Array.isArray(type) ? 'array' : type) ); } if ( vnode.ref !== undefined && typeof vnode.ref != 'function' && typeof vnode.ref != 'object' && !('$$typeof' in vnode) // allow string refs when preact-compat is installed ) { throw new Error( `Component's "ref" property should be a function, or an object created ` + `by createRef(), but got [${typeof vnode.ref}] instead\n` + serializeVNode(vnode) + `\n\n${getOwnerStack(vnode)}` ); } if (typeof vnode.type == 'string') { for (const key in vnode.props) { if ( key[0] === 'o' && key[1] === 'n' && typeof vnode.props[key] != 'function' && vnode.props[key] != null ) { throw new Error( `Component's "${key}" property should be a function, ` + `but got [${typeof vnode.props[key]}] instead\n` + serializeVNode(vnode) + `\n\n${getOwnerStack(vnode)}` ); } } } // Check prop-types if available if (typeof vnode.type == 'function' && vnode.type.propTypes) { if ( vnode.type.displayName === 'Lazy' && warnedComponents && !warnedComponents.lazyPropTypes.has(vnode.type) ) { const m = 'PropTypes are not supported on lazy(). Use propTypes on the wrapped component itself. '; try { const lazyVNode = vnode.type(); warnedComponents.lazyPropTypes.set(vnode.type, true); console.warn( m + `Component wrapped in lazy() is ${getDisplayName(lazyVNode)}` ); } catch (promise) { console.warn( m + "We will log the wrapped component's name once it is loaded." ); } } /* eslint-disable-next-line */ const { ref: _ref, ...props } = vnode.props; checkPropTypes( vnode.type.propTypes, props, 'prop', getDisplayName(vnode), () => getOwnerStack(vnode) ); } if (oldBeforeDiff) oldBeforeDiff(vnode); }; let renderCount = 0; let currentComponent; options._render = vnode => { if (oldRender) { oldRender(vnode); } hooksAllowed = true; const nextComponent = vnode._component; if (nextComponent === currentComponent) { renderCount++; } else { renderCount = 1; } if (renderCount >= 25) { throw new Error( `Too many re-renders. This is limited to prevent an infinite loop ` + `which may lock up your browser. The component causing this is: ${getDisplayName( vnode )}` ); } currentComponent = nextComponent; }; options._hook = (comp, index, type) => { if (!comp || !hooksAllowed) { throw new Error('Hook can only be invoked from render methods.'); } if (oldHook) oldHook(comp, index, type); }; // Ideally we'd want to print a warning once per component, but we // don't have access to the vnode that triggered it here. As a // compromise and to avoid flooding the console with warnings we // print each deprecation warning only once. const warn = (property, message) => ({ get() { const key = 'get' + property + message; if (deprecations && deprecations.indexOf(key) < 0) { deprecations.push(key); console.warn(`getting vnode.${property} is deprecated, ${message}`); } }, set() { const key = 'set' + property + message; if (deprecations && deprecations.indexOf(key) < 0) { deprecations.push(key); console.warn(`setting vnode.${property} is not allowed, ${message}`); } } }); const deprecatedAttributes = { nodeName: warn('nodeName', 'use vnode.type'), attributes: warn('attributes', 'use vnode.props'), children: warn('children', 'use vnode.props.children') }; const deprecatedProto = Object.create({}, deprecatedAttributes); options.vnode = vnode => { const props = vnode.props; if ( vnode.type !== null && props != null && ('__source' in props || '__self' in props) ) { const newProps = (vnode.props = {}); for (let i in props) { const v = props[i]; if (i === '__source') vnode.__source = v; else if (i === '__self') vnode.__self = v; else newProps[i] = v; } } // eslint-disable-next-line vnode.__proto__ = deprecatedProto; if (oldVnode) oldVnode(vnode); }; options.diffed = vnode => { const { type, _parent: parent } = vnode; // Check if the user passed plain objects as children. Note that we cannot // move this check into `options.vnode` because components can receive // children in any shape they want (e.g. // `{{ foo: 123, bar: "abc" }}`). // Putting this check in `options.diffed` ensures that // `vnode._children` is set and that we only validate the children // that were actually rendered. if (vnode._children) { vnode._children.forEach(child => { if (typeof child === 'object' && child && child.type === undefined) { const keys = Object.keys(child).join(','); throw new Error( `Objects are not valid as a child. Encountered an object with the keys {${keys}}.` + `\n\n${getOwnerStack(vnode)}` ); } }); } if (vnode._component === currentComponent) { renderCount = 0; } if ( typeof type === 'string' && (isTableElement(type) || type === 'p' || type === 'a' || type === 'button') ) { // Avoid false positives when Preact only partially rendered the // HTML tree. Whilst we attempt to include the outer DOM in our // validation, this wouldn't work on the server for // `preact-render-to-string`. There we'd otherwise flood the terminal // with false positives, which we'd like to avoid. let domParentName = getClosestDomNodeParentName(parent); if (domParentName !== '' && isTableElement(type)) { if ( type === 'table' && // Tables can be nested inside each other if it's inside a cell. // See https://developer.mozilla.org/en-US/docs/Learn/HTML/Tables/Advanced#nesting_tables domParentName !== 'td' && isTableElement(domParentName) ) { console.error( 'Improper nesting of table. Your should not have a table-node parent.' + serializeVNode(vnode) + `\n\n${getOwnerStack(vnode)}` ); } else if ( (type === 'thead' || type === 'tfoot' || type === 'tbody') && domParentName !== 'table' ) { console.error( 'Improper nesting of table. Your should have a
parent.' + serializeVNode(vnode) + `\n\n${getOwnerStack(vnode)}` ); } else if ( type === 'tr' && domParentName !== 'thead' && domParentName !== 'tfoot' && domParentName !== 'tbody' ) { console.error( 'Improper nesting of table. Your should have a parent.' + serializeVNode(vnode) + `\n\n${getOwnerStack(vnode)}` ); } else if (type === 'td' && domParentName !== 'tr') { console.error( 'Improper nesting of table. Your parent.' + serializeVNode(vnode) + `\n\n${getOwnerStack(vnode)}` ); } else if (type === 'th' && domParentName !== 'tr') { console.error( 'Improper nesting of table. Your .' + serializeVNode(vnode) + `\n\n${getOwnerStack(vnode)}` ); } } else if (type === 'p') { let illegalDomChildrenTypes = getDomChildren(vnode).filter(childType => ILLEGAL_PARAGRAPH_CHILD_ELEMENTS.test(childType) ); if (illegalDomChildrenTypes.length) { console.error( 'Improper nesting of paragraph. Your

should not have ' + illegalDomChildrenTypes.join(', ') + ' as child-elements.' + serializeVNode(vnode) + `\n\n${getOwnerStack(vnode)}` ); } } else if (type === 'a' || type === 'button') { if (getDomChildren(vnode).indexOf(type) !== -1) { console.error( `Improper nesting of interactive content. Your <${type}>` + ` should not have other ${type === 'a' ? 'anchor' : 'button'}` + ' tags as child-elements.' + serializeVNode(vnode) + `\n\n${getOwnerStack(vnode)}` ); } } } hooksAllowed = false; if (oldDiffed) oldDiffed(vnode); if (vnode._children != null) { const keys = []; for (let i = 0; i < vnode._children.length; i++) { const child = vnode._children[i]; if (!child || child.key == null) continue; const key = child.key; if (keys.indexOf(key) !== -1) { console.error( 'Following component has two or more children with the ' + `same key attribute: "${key}". This may cause glitches and misbehavior ` + 'in rendering process. Component: \n\n' + serializeVNode(vnode) + `\n\n${getOwnerStack(vnode)}` ); // Break early to not spam the console break; } keys.push(key); } } if (vnode._component != null && vnode._component.__hooks != null) { // Validate that none of the hooks in this component contain arguments that are NaN. // This is a common mistake that can be hard to debug, so we want to catch it early. const hooks = vnode._component.__hooks._list; if (hooks) { for (let i = 0; i < hooks.length; i += 1) { const hook = hooks[i]; if (hook._args) { for (let j = 0; j < hook._args.length; j++) { const arg = hook._args[j]; if (isNaN(arg)) { const componentName = getDisplayName(vnode); console.warn( `Invalid argument passed to hook. Hooks should not be called with NaN in the dependency array. Hook index ${i} in component ${componentName} was called with NaN.` ); } } } } } } }; } const setState = Component.prototype.setState; Component.prototype.setState = function (update, callback) { if (this._vnode == null) { // `this._vnode` will be `null` during componentWillMount. But it // is perfectly valid to call `setState` during cWM. So we // need an additional check to verify that we are dealing with a // call inside constructor. if (this.state == null) { console.warn( `Calling "this.setState" inside the constructor of a component is a ` + `no-op and might be a bug in your application. Instead, set ` + `"this.state = {}" directly.\n\n${getOwnerStack(getCurrentVNode())}` ); } } return setState.call(this, update, callback); }; function isTableElement(type) { return ( type === 'table' || type === 'tfoot' || type === 'tbody' || type === 'thead' || type === 'td' || type === 'tr' || type === 'th' ); } const ILLEGAL_PARAGRAPH_CHILD_ELEMENTS = /^(address|article|aside|blockquote|details|div|dl|fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5|h6|header|hgroup|hr|main|menu|nav|ol|p|pre|search|section|table|ul)$/; const forceUpdate = Component.prototype.forceUpdate; Component.prototype.forceUpdate = function (callback) { if (this._vnode == null) { console.warn( `Calling "this.forceUpdate" inside the constructor of a component is a ` + `no-op and might be a bug in your application.\n\n${getOwnerStack( getCurrentVNode() )}` ); } else if (this._parentDom == null) { console.warn( `Can't call "this.forceUpdate" on an unmounted component. This is a no-op, ` + `but it indicates a memory leak in your application. To fix, cancel all ` + `subscriptions and asynchronous tasks in the componentWillUnmount method.` + `\n\n${getOwnerStack(this._vnode)}` ); } return forceUpdate.call(this, callback); }; /** * Serialize a vnode tree to a string * @param {import('./internal').VNode} vnode * @returns {string} */ export function serializeVNode(vnode) { let { props } = vnode; let name = getDisplayName(vnode); let attrs = ''; for (let prop in props) { if (props.hasOwnProperty(prop) && prop !== 'children') { let value = props[prop]; // If it is an object but doesn't have toString(), use Object.toString if (typeof value == 'function') { value = `function ${value.displayName || value.name}() {}`; } value = Object(value) === value && !value.toString ? Object.prototype.toString.call(value) : value + ''; attrs += ` ${prop}=${JSON.stringify(value)}`; } } let children = props.children; return `<${name}${attrs}${ children && children.length ? '>..' : ' />' }`; } options._hydrationMismatch = (newVNode, excessDomChildren) => { const { type } = newVNode; const availableTypes = excessDomChildren .map(child => child && child.localName) .filter(Boolean); console.error( `Expected a DOM node of type "${type}" but found "${availableTypes.join(', ')}" as available DOM-node(s), this is caused by the SSR'd HTML containing different DOM-nodes compared to the hydrated one.\n\n${getOwnerStack(newVNode)}` ); }; ================================================ FILE: debug/src/index.d.ts ================================================ import { VNode } from '../../src/index'; /** * Return the component stack that was captured up to this point. */ export function captureOwnerStack(): string; /** * Get the currently rendered `vnode` */ export function getCurrentVNode(): VNode | null; /** * Get human readable name of the component/dom node */ export function getDisplayName(vnode: VNode): string; /** * Return the component stack that was captured up to this point. */ export function getOwnerStack(vnode: VNode): string; /** * Setup code to capture the component trace while rendering. Note that * we cannot simply traverse `vnode._parent` upwards, because we have some * debug messages for `this.setState` where the `vnode` is `undefined`. */ export function setupComponentStack(): void; /** * Reset the history of which prop type warnings have been logged. */ export function resetPropWarnings(): void; ================================================ FILE: debug/src/index.js ================================================ import { initDebug } from './debug'; import 'preact/devtools'; initDebug(); export { resetPropWarnings } from './check-props'; export { captureOwnerStack, getCurrentVNode, getDisplayName, getOwnerStack, setupComponentStack } from './component-stack'; ================================================ FILE: debug/src/internal.d.ts ================================================ import { Component, PreactElement, VNode, Options } from '../../src/internal'; export { Component, PreactElement, VNode, Options }; export interface DevtoolsInjectOptions { /** 1 = DEV, 0 = production */ bundleType: 1 | 0; /** The devtools enable different features for different versions of react */ version: string; /** Informative string, currently unused in the devtools */ rendererPackageName: string; /** Find the root dom node of a vnode */ findHostInstanceByFiber(vnode: VNode): HTMLElement | null; /** Find the closest vnode given a dom node */ findFiberByHostInstance(instance: HTMLElement): VNode | null; } export interface DevtoolsUpdater { setState(objOrFn: any): void; forceUpdate(): void; setInState(path: Array, value: any): void; setInProps(path: Array, value: any): void; setInContext(): void; } export type NodeType = 'Composite' | 'Native' | 'Wrapper' | 'Text'; export interface DevtoolData { nodeType: NodeType; // Component type type: any; name: string; ref: any; key: string | number; updater: DevtoolsUpdater | null; text: string | number | null; state: any; props: any; children: VNode[] | string | number | null; publicInstance: PreactElement | Text | Component; memoizedInteractions: any[]; actualDuration: number; actualStartTime: number; treeBaseDuration: number; } export type EventType = | 'unmount' | 'rootCommitted' | 'root' | 'mount' | 'update' | 'updateProfileTimes'; export interface DevtoolsEvent { data?: DevtoolData; internalInstance: VNode; renderer: string; type: EventType; } export interface DevtoolsHook { _renderers: Record; _roots: Set; on(ev: string, listener: () => void): void; emit(ev: string, data?: object): void; helpers: Record; getFiberRoots(rendererId: string): Set; inject(config: DevtoolsInjectOptions): string; onCommitFiberRoot(rendererId: string, root: VNode): void; onCommitFiberUnmount(rendererId: string, vnode: VNode): void; } export interface DevtoolsWindow extends Window { /** * If the devtools extension is installed it will inject this object into * the dom. This hook handles all communications between preact and the * devtools panel. */ __REACT_DEVTOOLS_GLOBAL_HOOK__?: DevtoolsHook; } ================================================ FILE: debug/src/util.js ================================================ export const assign = Object.assign; export function isNaN(value) { return value !== value; } ================================================ FILE: debug/test/browser/component-stack-2.test.jsx ================================================ import { createElement, render, Component } from 'preact'; import 'preact/debug'; import { setupScratch, teardown } from '../../../test/_util/helpers'; import { vi } from 'vitest'; // This test is not part of component-stack.test.js to avoid it being // transpiled with '@babel/plugin-transform-react-jsx-source' enabled. describe('component stack', () => { /** @type {HTMLDivElement} */ let scratch; let errors = []; let warnings = []; beforeEach(() => { scratch = setupScratch(); errors = []; warnings = []; vi.spyOn(console, 'error').mockImplementation(e => errors.push(e)); vi.spyOn(console, 'warn').mockImplementation(w => warnings.push(w)); }); afterEach(() => { console.error.mockRestore(); console.warn.mockRestore(); teardown(scratch); }); it('should print a warning when "@babel/plugin-transform-react-jsx-source" is not installed', () => { function Foo() { return ; } class Thrower extends Component { constructor(props) { super(props); this.setState({ foo: 1 }); } render() { return

foo
; } } render(, scratch); expect( warnings[0].indexOf('@babel/plugin-transform-react-jsx-source') > -1 ).to.equal(true); }); }); ================================================ FILE: debug/test/browser/component-stack.test.jsx ================================================ import { createElement, render, Component } from 'preact'; import 'preact/debug'; import { vi } from 'vitest'; import { setupScratch, teardown } from '../../../test/_util/helpers'; describe('component stack', () => { /** @type {HTMLDivElement} */ let scratch; let errors = []; let warnings = []; const getStack = arr => arr[0].split('\n\n')[1]; beforeEach(() => { scratch = setupScratch(); errors = []; warnings = []; vi.spyOn(console, 'error').mockImplementation(e => errors.push(e)); vi.spyOn(console, 'warn').mockImplementation(w => warnings.push(w)); }); afterEach(() => { vi.resetAllMocks(); teardown(scratch); }); it('should print component stack', () => { function Foo() { return ; } class Thrower extends Component { constructor(props) { super(props); this.setState({ foo: 1 }); } render() { return
foo
; } } render(, scratch); // This has a JSX transform warning, so we need to remove it warnings.shift(); let lines = getStack(warnings).split('\n'); expect(lines[0].indexOf('Thrower') > -1).to.equal(true); expect(lines[1].indexOf('Foo') > -1).to.equal(true); }); it('should only print owners', () => { function Foo(props) { return
{props.children}
; } function Bar() { return ( ); } class Thrower extends Component { render() { return (
should have a
should have a
foo
); } } render(, scratch); let lines = getStack(errors).split('\n'); expect(lines[0].indexOf('tr') > -1).to.equal(true); expect(lines[1].indexOf('Thrower') > -1).to.equal(true); expect(lines[2].indexOf('Bar') > -1).to.equal(true); }); it('should not print a warning when "@babel/plugin-transform-react-jsx-source" is installed', () => { function Thrower() { throw new Error('foo'); } try { render(, scratch); } catch {} expect(warnings.join(' ')).to.not.include( '@babel/plugin-transform-react-jsx-source' ); }); }); ================================================ FILE: debug/test/browser/debug-compat.test.jsx ================================================ import { createElement, render, createRef } from 'preact'; import { setupScratch, teardown } from '../../../test/_util/helpers'; import './fakeDevTools'; import 'preact/debug'; import * as PropTypes from 'prop-types'; // eslint-disable-next-line no-duplicate-imports import { resetPropWarnings } from 'preact/debug'; import { forwardRef, createPortal } from 'preact/compat'; import { vi } from 'vitest'; const h = createElement; describe('debug compat', () => { let scratch; let root; let errors = []; let warnings = []; beforeEach(() => { errors = []; warnings = []; scratch = setupScratch(); vi.spyOn(console, 'error').mockImplementation(e => errors.push(e)); vi.spyOn(console, 'warn').mockImplementation(w => warnings.push(w)); root = document.createElement('div'); document.body.appendChild(root); }); afterEach(() => { /** @type {*} */ console.error.mockRestore(); console.warn.mockRestore(); teardown(scratch); document.body.removeChild(root); }); describe('portals', () => { it('should not throw an invalid render argument for a portal.', () => { function Foo(props) { return
{createPortal(props.children, root)}
; } expect(() => render(foobar, scratch)).not.to.throw(); }); }); describe('PropTypes', () => { beforeEach(() => { resetPropWarnings(); }); it('should not fail if ref is passed to comp wrapped in forwardRef', () => { // This test ensures compat with airbnb/prop-types-exact, mui exact prop types util, etc. const Foo = forwardRef(function Foo(props, ref) { return

{props.text}

; }); Foo.propTypes = { text: PropTypes.string.isRequired, ref(props) { if ('ref' in props) { throw new Error( 'ref should not be passed to prop-types valiation!' ); } } }; const ref = createRef(); render(, scratch); expect(console.error).not.toHaveBeenCalled(); expect(ref.current).to.not.be.undefined; }); }); }); ================================================ FILE: debug/test/browser/debug-hooks.test.jsx ================================================ import { createElement, render, Component } from 'preact'; import { useState, useEffect } from 'preact/hooks'; import 'preact/debug'; import { act } from 'preact/test-utils'; import { setupScratch, teardown } from '../../../test/_util/helpers'; import { vi } from 'vitest'; describe('debug with hooks', () => { let scratch; let errors = []; let warnings = []; beforeEach(() => { errors = []; warnings = []; scratch = setupScratch(); vi.spyOn(console, 'error').mockImplementation(e => errors.push(e)); vi.spyOn(console, 'warn').mockImplementation(w => warnings.push(w)); }); afterEach(() => { console.error.mockRestore(); console.warn.mockRestore(); teardown(scratch); }); it('should throw an error when using a hook outside a render', () => { class Foo extends Component { componentWillMount() { useState(); } render() { return this.props.children; } } class App extends Component { render() { return

test

; } } const fn = () => act(() => render( , scratch ) ); expect(fn).to.throw(/Hook can only be invoked from render/); }); it('should throw an error when invoked outside of a component', () => { function foo() { useEffect(() => {}); // Pretend to use a hook return

test

; } const fn = () => act(() => { render(foo(), scratch); }); expect(fn).to.throw(/Hook can only be invoked from render/); }); it('should throw an error when invoked outside of a component before render', () => { function Foo(props) { useEffect(() => {}); // Pretend to use a hook return props.children; } const fn = () => act(() => { useState(); render(Hello!, scratch); }); expect(fn).to.throw(/Hook can only be invoked from render/); }); it('should throw an error when invoked outside of a component after render', () => { function Foo(props) { useEffect(() => {}); // Pretend to use a hook return props.children; } const fn = () => act(() => { render(Hello!, scratch); useState(); }); expect(fn).to.throw(/Hook can only be invoked from render/); }); it('should throw an error when invoked inside an effect callback', () => { function Foo(props) { useEffect(() => { useState(); }); return props.children; } const fn = () => act(() => { render(Hello!, scratch); }); expect(fn).to.throw(/Hook can only be invoked from render/); }); }); ================================================ FILE: debug/test/browser/debug-suspense.test.jsx ================================================ import { createElement, render, lazy, Suspense } from 'preact/compat'; import 'preact/debug'; import { setupRerender } from 'preact/test-utils'; import { setupScratch, teardown, serializeHtml } from '../../../test/_util/helpers'; import { vi } from 'vitest'; describe('debug with suspense', () => { /** @type {HTMLDivElement} */ let scratch; let rerender; let errors = []; let warnings = []; beforeEach(() => { errors = []; warnings = []; scratch = setupScratch(); rerender = setupRerender(); vi.spyOn(console, 'error').mockImplementation(e => errors.push(e)); vi.spyOn(console, 'warn').mockImplementation(w => warnings.push(w)); }); afterEach(() => { console.error.mockRestore(); console.warn.mockRestore(); teardown(scratch); }); it('should throw on missing ', () => { function Foo() { throw Promise.resolve(); } expect(() => render(, scratch)).to.throw; }); it('should throw an error when using lazy and missing Suspense', () => { const Foo = () =>
Foo
; const LazyComp = lazy( () => new Promise(resolve => resolve({ default: Foo })) ); const fn = () => { render(, scratch); }; expect(fn).to.throw(/Missing Suspense/gi); }); describe('PropTypes', () => { it('should validate propTypes inside lazy()', () => { function Baz(props) { return

{props.unhappy}

; } Baz.propTypes = { unhappy: function alwaysThrows(obj, key) { if (obj[key] === 'signal') throw Error('got prop inside lazy()'); } }; const loader = Promise.resolve({ default: Baz }); const LazyBaz = lazy(() => loader); const suspense = ( fallback...
}> ); render(suspense, scratch); rerender(); // render fallback expect(console.error).not.toHaveBeenCalled(); expect(serializeHtml(scratch)).to.equal('
fallback...
'); return loader.then(() => { rerender(); expect(errors.length).to.equal(1); expect(errors[0].includes('got prop')).to.equal(true); expect(serializeHtml(scratch)).to.equal('

signal

'); }); }); describe('warn for PropTypes on lazy()', () => { it('should log the function name', () => { const loader = Promise.resolve({ default: function MyLazyLoaded() { return
Hi there
; } }); const FakeLazy = lazy(() => loader); FakeLazy.propTypes = {}; const suspense = ( fallback...
}> ); render(suspense, scratch); rerender(); // Render fallback expect(serializeHtml(scratch)).to.equal('
fallback...
'); return loader.then(() => { rerender(); expect(console.warn).toHaveBeenCalledTimes(2); expect(warnings[1].includes('MyLazyLoaded')).to.equal(true); expect(serializeHtml(scratch)).to.equal('
Hi there
'); }); }); it('should log the displayName', () => { function MyLazyLoadedComponent() { return
Hi there
; } MyLazyLoadedComponent.displayName = 'HelloLazy'; const loader = Promise.resolve({ default: MyLazyLoadedComponent }); const FakeLazy = lazy(() => loader); FakeLazy.propTypes = {}; const suspense = ( fallback...
}> ); render(suspense, scratch); rerender(); // Render fallback expect(serializeHtml(scratch)).to.equal('
fallback...
'); return loader.then(() => { rerender(); expect(console.warn).toHaveBeenCalledTimes(2); expect(warnings[1].includes('HelloLazy')).to.equal(true); expect(serializeHtml(scratch)).to.equal('
Hi there
'); }); }); it("should not log a component if lazy loader's Promise rejects", () => { const loader = Promise.reject(new Error('Hey there')); const FakeLazy = lazy(() => loader); FakeLazy.propTypes = {}; render( fallback...
}> , scratch ); rerender(); // Render fallback expect(serializeHtml(scratch)).to.equal('
fallback...
'); return loader.catch(() => { try { rerender(); } catch (e) { // Ignore the loader's bubbling error } // Called once on initial render, and again when promise rejects expect(console.warn).toHaveBeenCalledTimes(2); }); }); it("should not log a component if lazy's loader throws", () => { const FakeLazy = lazy(() => { throw new Error('Hello'); }); FakeLazy.propTypes = {}; let error; try { render( fallback...
}> , scratch ); } catch (e) { error = e; } expect(console.warn).toHaveBeenCalledOnce(); expect(error).not.to.be.undefined; expect(error.message).to.eql('Hello'); }); }); }); }); ================================================ FILE: debug/test/browser/debug.options.test.jsx ================================================ import { vnodeSpy, rootSpy, beforeDiffSpy, hookSpy, afterDiffSpy, catchErrorSpy } from '../../../test/_util/optionSpies'; import { createElement, render, Component } from 'preact'; import { useState } from 'preact/hooks'; import { setupRerender } from 'preact/test-utils'; import 'preact/debug'; import { setupScratch, teardown } from '../../../test/_util/helpers'; import { vi } from 'vitest'; describe('debug options', () => { /** @type {HTMLDivElement} */ let scratch; /** @type {() => void} */ let rerender; /** @type {(count: number) => void} */ let setCount; /** @type {import('vitest').VitestUtils | undefined} */ let clock; beforeEach(() => { scratch = setupScratch(); rerender = setupRerender(); vnodeSpy.mockClear(); rootSpy.mockClear(); beforeDiffSpy.mockClear(); hookSpy.mockClear(); afterDiffSpy.mockClear(); catchErrorSpy.mockClear(); }); afterEach(() => { teardown(scratch); if (clock) vi.useRealTimers(); }); class ClassApp extends Component { constructor() { super(); this.state = { count: 0 }; setCount = count => this.setState({ count }); } render() { return
{this.state.count}
; } } it('should call old options on mount', () => { render(, scratch); expect(vnodeSpy).toHaveBeenCalled(); expect(rootSpy).toHaveBeenCalled(); expect(beforeDiffSpy).toHaveBeenCalled(); expect(afterDiffSpy).toHaveBeenCalled(); }); it('should call old options on update', () => { render(, scratch); setCount(1); rerender(); expect(vnodeSpy).toHaveBeenCalled(); expect(rootSpy).toHaveBeenCalled(); expect(beforeDiffSpy).toHaveBeenCalled(); expect(afterDiffSpy).toHaveBeenCalled(); }); it('should call old options on unmount', () => { render(, scratch); render(null, scratch); expect(vnodeSpy).toHaveBeenCalled(); expect(rootSpy).toHaveBeenCalled(); expect(beforeDiffSpy).toHaveBeenCalled(); expect(afterDiffSpy).toHaveBeenCalled(); }); it('should call old hook options for hook components', () => { function HookApp() { const [count, realSetCount] = useState(0); setCount = realSetCount; return
{count}
; } render(, scratch); expect(hookSpy).toHaveBeenCalled(); }); it('should call old options on error', () => { const e = new Error('test'); class ErrorApp extends Component { constructor() { super(); this.state = { error: true }; } componentDidCatch() { this.setState({ error: false }); } render() { return ; } } function Throw({ error }) { if (error) { throw e; } else { return
no error
; } } clock = vi.useFakeTimers(); render(, scratch); rerender(); expect(catchErrorSpy).toHaveBeenCalled(); // we expect to throw after setTimeout to trigger a window.onerror // this is to ensure react compat (i.e. with next.js' dev overlay) expect(() => clock.advanceTimersByTime(0)).to.throw(e); }); }); ================================================ FILE: debug/test/browser/debug.test.jsx ================================================ import { createElement, render, createRef, Component, Fragment, hydrate } from 'preact'; import { useState } from 'preact/hooks'; import { setupScratch, teardown } from '../../../test/_util/helpers'; import './fakeDevTools'; import 'preact/debug'; import { setupRerender } from 'preact/test-utils'; import { vi } from 'vitest'; const h = createElement; describe('debug', () => { /** @type {HTMLDivElement} */ let scratch; let errors = []; let warnings = []; let rerender; beforeEach(() => { errors = []; warnings = []; scratch = setupScratch(); rerender = setupRerender(); vi.spyOn(console, 'error').mockImplementation(e => errors.push(e)); vi.spyOn(console, 'warn').mockImplementation(w => warnings.push(w)); }); afterEach(() => { /** @type {*} */ console.error.mockRestore(); console.warn.mockRestore(); teardown(scratch); }); it('should initialize devtools', () => { expect(window.__PREACT_DEVTOOLS__.attachPreact).toHaveBeenCalled(); }); it('should print an error on rendering on undefined parent', () => { let fn = () => render(
, undefined); expect(fn).to.throw(/render/); }); it('should print an error on rendering on invalid parent', () => { let fn = () => render(
, 6); expect(fn).to.throw(/valid HTML node/); expect(fn).to.throw(/
{ const App = () =>
; let fn = () => render(, 6); expect(fn).to.throw(/ render(, {}); expect(fn).to.throw(/ render(, 'badroot'); expect(fn).to.throw(/ { class App extends Component { render() { return
; } } let fn = () => render(, 6); expect(fn).to.throw(/ { let fn = () => render(h(undefined), scratch); expect(fn).to.throw(/createElement/); }); it('should print an error on invalid object component', () => { let fn = () => render(h({}), scratch); expect(fn).to.throw(/createElement/); }); it('should print an error when component is an array', () => { let fn = () => render(h([
]), scratch); expect(fn).to.throw(/createElement/); }); it('should print an error on double jsx conversion', () => { let Foo =
; let fn = () => render(h(), scratch); expect(fn).to.throw(/JSX twice/); }); it('should add __source to the vnode in debug mode.', () => { const vnode = h('div', { __source: { fileName: 'div.jsx', lineNumber: 3 } }); expect(vnode.__source).to.deep.equal({ fileName: 'div.jsx', lineNumber: 3 }); expect(vnode.props.__source).to.be.undefined; }); it('should add __self to the vnode in debug mode.', () => { const vnode = h('div', { __self: {} }); expect(vnode.__self).to.deep.equal({}); expect(vnode.props.__self).to.be.undefined; }); it('should warn when accessing certain attributes', () => { const vnode = h('div', null); // Push into an array to avoid empty statements being dead code eliminated const res = []; res.push(vnode); res.push(vnode.attributes); expect(console.warn).toHaveBeenCalledOnce(); expect(console.warn.mock.calls[0]).to.match(/use vnode.props/); res.push(vnode.nodeName); expect(console.warn).toHaveBeenCalledTimes(2); expect(console.warn.mock.calls[1]).to.match(/use vnode.type/); res.push(vnode.children); expect(console.warn).toHaveBeenCalledTimes(3); expect(console.warn.mock.calls[2]).to.match(/use vnode.props.children/); // Should only warn once res.push(vnode.attributes); expect(console.warn).toHaveBeenCalledTimes(3); res.push(vnode.nodeName); expect(console.warn).toHaveBeenCalledTimes(3); res.push(vnode.children); expect(console.warn).toHaveBeenCalledTimes(3); vnode.attributes = {}; expect(console.warn.mock.calls[3]).to.match(/use vnode.props/); vnode.nodeName = ''; expect(console.warn.mock.calls[4]).to.match(/use vnode.type/); vnode.children = []; expect(console.warn.mock.calls[5]).to.match(/use vnode.props.children/); // Should only warn once vnode.attributes = {}; expect(console.warn.mock.calls.length).to.equal(6); vnode.nodeName = ''; expect(console.warn.mock.calls.length).to.equal(6); vnode.children = []; expect(console.warn.mock.calls.length).to.equal(6); // Mark res as used, otherwise it will be dead code eliminated expect(res.length).to.equal(7); }); it('should warn when calling setState inside the constructor', () => { class Foo extends Component { constructor(props) { super(props); this.setState({ foo: true }); } render() { return
foo
; } } render(, scratch); expect(console.warn).toHaveBeenCalledOnce(); expect(console.warn.mock.calls[0]).to.match(/no-op/); }); it('should NOT warn when calling setState inside the cWM', () => { class Foo extends Component { componentWillMount() { this.setState({ foo: true }); } render() { return
foo
; } } render(, scratch); expect(console.warn).not.toHaveBeenCalled(); }); it('should warn when calling forceUpdate inside the constructor', () => { class Foo extends Component { constructor(props) { super(props); this.forceUpdate(); } render() { return
foo
; } } render(, scratch); expect(console.warn).toHaveBeenCalledOnce(); expect(console.warn.mock.calls[0]).to.match(/no-op/); }); it('should warn when calling forceUpdate on an unmounted Component', () => { let forceUpdate; class Foo extends Component { constructor(props) { super(props); forceUpdate = () => this.forceUpdate(); } render() { return
foo
; } } render(, scratch); forceUpdate(); expect(console.warn).not.toHaveBeenCalled(); render(null, scratch); forceUpdate(); expect(console.warn).toHaveBeenCalledOnce(); expect(console.warn.mock.calls[0]).to.match(/no-op/); }); it('should print an error when child is a plain object', () => { let fn = () => render(
{{}}
, scratch); expect(fn).to.throw(/not valid/); }); it('should print an error on invalid refs', () => { let fn = () => render(
, scratch); expect(fn).to.throw(/createRef/); }); it('should not print for null as a handler', () => { let fn = () => render(
, scratch); expect(fn).not.to.throw(); }); it('should not print for undefined as a handler', () => { let fn = () => render(
, scratch); expect(fn).not.to.throw(); }); it('should not print for attributes starting with on for Components', () => { const Comp = () =>

online

; let fn = () => render(, scratch); expect(fn).not.to.throw(); }); it('should print an error on invalid handler', () => { let fn = () => render(
, scratch); expect(fn).to.throw(/"onclick" property should be a function/); }); it('should NOT print an error on valid refs', () => { let noop = () => {}; render(
, scratch); let ref = createRef(); render(
, scratch); expect(console.error).not.toHaveBeenCalled(); }); it('throws an error if a component rerenders too many times', () => { let rerenderCount = 0; function TestComponent({ loop = false }) { const [count, setCount] = useState(0); if (loop) { setCount(count + 1); } if (count > 30) { expect.fail( 'Repeated rerenders did not cause the expected error. This test is failing.' ); } rerenderCount += 1; return
; } expect(() => { render( , scratch ); }).to.throw(/Too many re-renders/); // 1 for first TestComponent + 24 for second TestComponent expect(rerenderCount).to.equal(25); }); it('does not throw an error if a component renders many times in different cycles', () => { let set; function TestComponent() { const [count, setCount] = useState(0); set = () => setCount(count + 1); return
{count}
; } render(, scratch); for (let i = 0; i < 30; i++) { set(); rerender(); } expect(scratch.innerHTML).to.equal('
30
'); }); describe('duplicate keys', () => { const List = props =>
    {props.children}
; const ListItem = props =>
  • {props.children}
  • ; it('should print an error on duplicate keys with DOM nodes', () => { render(
    , scratch ); expect(console.error).toHaveBeenCalledOnce(); }); it('should allow distinct object keys', () => { const A = { is: 'A' }; const B = { is: 'B' }; render(
    , scratch ); expect(console.error).not.toHaveBeenCalled(); }); it('should print an error for duplicate object keys', () => { const A = { is: 'A' }; render(
    , scratch ); expect(console.error).toHaveBeenCalledOnce(); }); it('should print an error on duplicate keys with Components', () => { function App() { return ( a b d d ); } render(, scratch); expect(console.error).toHaveBeenCalledOnce(); }); it('should print an error on duplicate keys with Fragments', () => { function App() { return ( a b {/* Should be okay to duplicate keys since these are inside a Fragment */} c d e f
    sibling
    ); } render(, scratch); expect(console.error).toHaveBeenCalledTimes(2); }); }); describe('table markup', () => { it('missing ///', () => { const Table = () => (
    ); render(
    hi
    , scratch); expect(console.error).toHaveBeenCalledOnce(); }); it('missing
    with ', () => { const Table = () => (
    ); render(
    hi
    , scratch); expect(console.error).toHaveBeenCalledOnce(); }); it('missing
    with ', () => { const Table = () => (
    ); render(
    hi
    , scratch); expect(console.error).toHaveBeenCalledOnce(); }); it('missing
    with ', () => { const Table = () => (
    ); render(
    hi
    , scratch); expect(console.error).toHaveBeenCalledOnce(); }); it('missing ', () => { const Table = () => (
    Hi
    ); render(, scratch); expect(console.error).toHaveBeenCalledOnce(); }); it('missing with td component', () => { const Cell = ({ children }) => ; const Table = () => (
    {children}
    Hi
    ); render(, scratch); expect(console.error).toHaveBeenCalledOnce(); }); it('missing with th component', () => { const Cell = ({ children }) => ; const Table = () => (
    {children}
    Hi
    ); render(, scratch); expect(console.error).toHaveBeenCalledOnce(); }); it('Should accept ', () => { const Table = () => (
    instead of in
    Hi
    ); render(, scratch); expect(console.error).not.toHaveBeenCalled(); }); it('Accepts well formed table with TD components', () => { const Cell = ({ children }) => ; const Table = () => (
    {children}
    Body
    Head
    Body
    ); render(, scratch); expect(console.error).not.toHaveBeenCalled(); }); it('Accepts well formed table', () => { const Table = () => (
    Head
    Body
    Body
    ); render(, scratch); expect(console.error).not.toHaveBeenCalled(); }); it('Accepts minimal well formed table', () => { const Table = () => (
    Head
    Body
    ); render(, scratch); expect(console.error).not.toHaveBeenCalled(); }); it('should include DOM parents outside of root node', () => { const Table = () => ( ); const table = document.createElement('table'); scratch.appendChild(table); render(
    Head
    , table); expect(console.error).not.toHaveBeenCalled(); }); it('should warn for improper nested table', () => { const Table = () => (
    ); render(, scratch); expect(console.error).toHaveBeenCalledOnce(); }); it('accepts valid nested tables', () => { const Table = () => (
    foo
    cell1 cell2 cell3
    bar
    ); render(, scratch); expect(console.error).not.toHaveBeenCalled(); }); }); describe('paragraph nesting', () => { it('should not warn a regular text paragraph', () => { const Paragraph = () =>

    Hello world

    ; render(, scratch); expect(console.error).not.toHaveBeenCalled(); }); it('should not crash for an empty pragraph', () => { const Paragraph = () =>

    ; render(, scratch); expect(console.error).not.toHaveBeenCalled(); }); it('should warn for nesting illegal dom-nodes under a paragraph', () => { const Paragraph = () => (

    Hello world

    ); render(, scratch); expect(console.error).toHaveBeenCalledOnce(); }); it('should warn for nesting illegal dom-nodes under a paragraph with a parent', () => { const Paragraph = () => (

    Hello world

    ); render(, scratch); expect(console.error).toHaveBeenCalledOnce(); }); it('should warn for nesting illegal dom-nodes under a paragraph as func', () => { const Title = ({ children }) =>

    {children}

    ; const Paragraph = () => (

    Hello world

    ); render(, scratch); expect(console.error).toHaveBeenCalledOnce(); }); it('should not warn for nesting span under a paragraph', () => { const Paragraph = () => (

    Hello world

    ); render(, scratch); expect(console.error).not.toHaveBeenCalled(); }); }); describe('button nesting', () => { it('should not warn on a regular button', () => { const Button = () => ; render( ); render(; const Button = () => ( ); render( ); render( ); render(, scratch); expect(console.error).not.toHaveBeenCalled(); }); }); describe('Hydration mismatches', () => { it('Should warn us for a node mismatch', () => { scratch.innerHTML = '
    foo/div>'; const App = () => (

    foo

    ); hydrate(, scratch); expect(console.error).toHaveBeenCalledOnce(); expect(console.error).toHaveBeenCalledWith( expect.objectContaining( /Expected a DOM node of type "p" but found "span"/ ) ); }); it('Should not warn for a text-node mismatch', () => { scratch.innerHTML = '
    foo bar baz/div>'; const App = () => (
    foo {'bar'} {'baz'}
    ); hydrate(, scratch); expect(console.error).not.toHaveBeenCalled(); }); it('Should not warn for a well-formed tree', () => { scratch.innerHTML = '
    foobar
    '; const App = () => (
    foo bar
    ); hydrate(, scratch); expect(console.error).not.toHaveBeenCalled(); }); }); }); ================================================ FILE: debug/test/browser/fakeDevTools.js ================================================ window.__PREACT_DEVTOOLS__ = { attachPreact: vi.fn() }; ================================================ FILE: debug/test/browser/prop-types.test.js ================================================ import { createElement, render } from 'preact'; import { setupScratch, teardown, serializeHtml } from '../../../test/_util/helpers'; import './fakeDevTools'; import { resetPropWarnings } from 'preact/debug'; import * as PropTypes from 'prop-types'; import { jsxDEV as jsxDev } from 'preact/jsx-runtime'; import { vi } from 'vitest'; describe('PropTypes', () => { /** @type {HTMLDivElement} */ let scratch; let errors = []; let warnings = []; beforeEach(() => { errors = []; warnings = []; scratch = setupScratch(); vi.spyOn(console, 'error').mockImplementation(e => errors.push(e)); vi.spyOn(console, 'warn').mockImplementation(w => warnings.push(w)); }); afterEach(() => { /** @type {*} */ console.error.mockRestore(); console.warn.mockRestore(); teardown(scratch); }); beforeEach(() => { resetPropWarnings(); }); it("should fail if props don't match prop-types", () => { function Foo(props) { return jsxDev('h1', { children: props.text }); } Foo.propTypes = { text: PropTypes.string.isRequired }; render( jsxDev( Foo, { text: 123 }, null, // @ts-ignore false, // @ts-ignore { fileName: './debug/test/browser/debug.test.js', lineNumber: 41 }, // @ts-ignore 'self' ), scratch ); expect(console.error).toHaveBeenCalledOnce(); // The message here may change when the "prop-types" library is updated, // but we check it exactly to make sure all parameters were supplied // correctly. expect(console.error).toHaveBeenCalledOnce(); expect(console.error).toHaveBeenCalledWith( expect.stringMatching( /^Failed prop type: Invalid prop `text` of type `number` supplied to `Foo`, expected `string`\.\n {2}in Foo \(at (.*)[/\\]debug[/\\]test[/\\]browser[/\\]debug\.test\.js:[0-9]+\)$/m ) ); }); it('should only log a given prop type error once', () => { function Foo(props) { return jsxDev('h1', { children: props.text }); } Foo.propTypes = { text: PropTypes.string.isRequired, count: PropTypes.number }; // Trigger the same error twice. The error should only be logged // once. render(jsxDev(Foo, { text: 123 }), scratch); render(jsxDev(Foo, { text: 123 }), scratch); expect(console.error).toHaveBeenCalledOnce(); // Trigger a different error. This should result in a new log // message. console.error.mockClear(); render(jsxDev(Foo, { text: 'ok', count: '123' }), scratch); expect(console.error).toHaveBeenCalledOnce(); }); it('should render with error logged when validator gets signal and throws exception', () => { function Baz(props) { return jsxDev('h1', { children: props.unhappy }); } Baz.propTypes = { unhappy: function alwaysThrows(obj, key) { if (obj[key] === 'signal') throw Error('got prop'); } }; render(jsxDev(Baz, { unhappy: 'signal' }), scratch); expect(console.error).toHaveBeenCalledOnce(); expect(errors[0].includes('got prop')).to.equal(true); expect(serializeHtml(scratch)).to.equal('

    signal

    '); }); it('should not print to console when types are correct', () => { function Bar(props) { return jsxDev('h1', { children: props.text }); } Bar.propTypes = { text: PropTypes.string.isRequired }; render(jsxDev(Bar, { text: 'foo' }), scratch); expect(console.error).not.toHaveBeenCalled(); }); }); ================================================ FILE: debug/test/browser/serializeVNode.test.jsx ================================================ import { createElement, Component } from 'preact'; import { serializeVNode } from '../../src/debug'; describe('serializeVNode', () => { it("should prefer a function component's displayName", () => { function Foo() { return
    ; } Foo.displayName = 'Bar'; expect(serializeVNode()).to.equal(''); }); it("should prefer a class component's displayName", () => { class Bar extends Component { render() { return
    ; } } Bar.displayName = 'Foo'; expect(serializeVNode()).to.equal(''); }); it('should serialize vnodes without children', () => { expect(serializeVNode(
    )).to.equal('
    '); }); it('should serialize vnodes with children', () => { expect(serializeVNode(
    Hello World
    )).to.equal('
    ..
    '); }); it('should serialize components', () => { function Foo() { return
    ; } expect(serializeVNode()).to.equal(''); }); it('should serialize props', () => { expect(serializeVNode(
    )).to.equal('
    '); // Ensure that we have a predictable function name. Our test runner // creates an all inclusive bundle per file and the identifier // "noop" may have already been used. // eslint-disable-next-line func-style let noop = function noopFn() {}; expect(serializeVNode(
    )).to.equal( '
    ' ); function Foo(props) { return props.foo; } expect(serializeVNode()).to.equal( '' ); expect(serializeVNode(
    )).to.equal( '
    ' ); }); }); ================================================ FILE: debug/test/browser/validateHookArgs.test.jsx ================================================ import { createElement, render, createRef } from 'preact'; import { useState, useEffect, useLayoutEffect, useCallback, useMemo, useImperativeHandle } from 'preact/hooks'; import { setupRerender } from 'preact/test-utils'; import { setupScratch, teardown } from '../../../test/_util/helpers'; import 'preact/debug'; import { vi } from 'vitest'; describe('Hook argument validation', () => { /** * @param {string} name * @param {(arg: number) => void} hook */ function validateHook(name, hook) { const TestComponent = ({ initialValue }) => { const [value, setValue] = useState(initialValue); hook(value); return ( ); }; it(`should error if ${name} is mounted with NaN as an argument`, async () => { render(, scratch); expect(console.warn).toHaveBeenCalledOnce(); expect(console.warn.mock.calls[0]).to.match( /Hooks should not be called with NaN in the dependency array/ ); }); it(`should error if ${name} is updated with NaN as an argument`, async () => { render(, scratch); scratch.querySelector('button').click(); rerender(); expect(console.warn).toHaveBeenCalledOnce(); expect(console.warn.mock.calls[0]).to.match( /Hooks should not be called with NaN in the dependency array/ ); }); } /** @type {HTMLElement} */ let scratch; /** @type {() => void} */ let rerender; let warnings = []; beforeEach(() => { scratch = setupScratch(); rerender = setupRerender(); warnings = []; vi.spyOn(console, 'warn').mockImplementation(w => warnings.push(w)); }); afterEach(() => { teardown(scratch); console.warn.mockRestore(); }); validateHook('useEffect', arg => useEffect(() => {}, [arg])); validateHook('useLayoutEffect', arg => useLayoutEffect(() => {}, [arg])); validateHook('useCallback', arg => useCallback(() => {}, [arg])); validateHook('useMemo', arg => useMemo(() => {}, [arg])); const ref = createRef(); validateHook('useImperativeHandle', arg => { useImperativeHandle(ref, () => undefined, [arg]); }); }); ================================================ FILE: demo/contenteditable.jsx ================================================ import { useState } from 'preact/hooks'; export default function Contenteditable() { const [value, setValue] = useState("Hey there
    I'm editable!"); return (
    setValue(e.currentTarget.innerHTML)} dangerouslySetInnerHTML={{ __html: value }} />
    ); } ================================================ FILE: demo/context.jsx ================================================ // eslint-disable-next-line no-unused-vars import { Component, createContext } from 'preact'; const { Provider, Consumer } = createContext(); class ThemeProvider extends Component { state = { value: this.props.value }; onClick = () => { this.setState(prev => ({ value: prev.value === this.props.value ? this.props.next : this.props.value })); }; render() { return (
    {this.props.children}
    ); } } class Child extends Component { shouldComponentUpdate() { return false; } render() { return ( <>

    (blocked update)

    {this.props.children} ); } } export default class ContextDemo extends Component { render() { return ( {data => (

    current theme: {data}

    {data => (

    current sub theme: {data}

    )}
    )}
    ); } } ================================================ FILE: demo/devtools.jsx ================================================ import { Component, memo, Suspense, lazy } from 'react'; function Foo() { return
    I'm memoed
    ; } function LazyComp() { return
    I'm (fake) lazy loaded
    ; } const Lazy = lazy(() => Promise.resolve({ default: LazyComp })); const Memoed = memo(Foo); export default class DevtoolsDemo extends Component { render() { return (

    memo()

    functional component:

    lazy()

    functional component:

    Loading (fake) lazy loaded component...
    }>
    ); } } ================================================ FILE: demo/fragments.jsx ================================================ import { Component } from 'preact'; export default class FragmentComp extends Component { state = { number: 0 }; componentDidMount() { setInterval(_ => this.updateChildren(), 1000); } updateChildren() { this.setState(state => ({ number: state.number + 1 })); } render(props, state) { return (
    {state.number}
    <>
    one
    {state.number}
    three
    ); } } ================================================ FILE: demo/index.html ================================================ Preact Demo
    ================================================ FILE: demo/index.jsx ================================================ import { render, Component, Fragment } from 'preact'; // import renderToString from 'preact-render-to-string'; import './style.scss'; import { Router, Link } from 'preact-router'; import Pythagoras from './pythagoras'; import Spiral from './spiral'; import Reorder from './reorder'; import Todo from './todo'; import Fragments from './fragments'; import Context from './context'; import installLogger from './logger'; import ProfilerDemo from './profiler'; import KeyBug from './key_bug'; import StateOrderBug from './stateOrderBug'; import PeopleBrowser from './people'; import StyledComp from './styled-components'; import { initDevTools } from 'preact/devtools/src/devtools'; import { initDebug } from 'preact/debug/src/debug'; import DevtoolsDemo from './devtools'; import SuspenseDemo from './suspense'; import Redux from './redux'; import TextFields from './textFields'; import ReduxBug from './reduxUpdate'; import SuspenseRouterBug from './suspense-router'; import NestedSuspenseBug from './nested-suspense'; import Contenteditable from './contenteditable'; import { MobXDemo } from './mobx'; import Zustand from './zustand'; import ReduxToolkit from './redux-toolkit'; let isBenchmark = /(\/spiral|\/pythagoras|[#&]bench)/g.test( window.location.href ); if (!isBenchmark) { // eslint-disable-next-line no-console console.log('Enabling devtools and debug'); initDevTools(); initDebug(); } // mobx-state-tree fix window.setImmediate = setTimeout; class Home extends Component { render() { return (

    Hello

    ); } } class DevtoolsWarning extends Component { onClick = () => { window.location.reload(); }; render() { return ( ); } } class App extends Component { render({ url }) { return (
    {!isBenchmark ? : }
    {!isBenchmark ? : }
    ); } } function EmptyFragment() { return ; } // document.body.innerHTML = renderToString(); // document.body.firstChild.setAttribute('is-ssr', 'true'); installLogger( String(localStorage.LOG) === 'true' || location.href.match(/logger/), String(localStorage.CONSOLE) === 'true' || location.href.match(/console/) ); render(, document.body); ================================================ FILE: demo/key_bug.jsx ================================================ import { Component } from 'preact'; function Foo(props) { return
    This is: {props.children}
    ; } export default class KeyBug extends Component { constructor() { super(); this.onClick = this.onClick.bind(this); this.state = { active: false }; } onClick() { this.setState(prev => ({ active: !prev.active })); } render() { return (
    {this.state.active && foo}

    Hello World


    bar bar
    ); } } ================================================ FILE: demo/list.jsx ================================================ import { h, render } from 'preact'; import htm from 'htm'; import './style.css'; const html = htm.bind(h); const createRoot = parent => ({ render: v => render(v, parent) }); function List({ items, renders, useKeys, useCounts, update }) { const toggleKeys = () => update({ useKeys: !useKeys }); const toggleCounts = () => update({ useCounts: !useCounts }); const swap = () => { const u = { items: items.slice() }; u.items[1] = items[8]; u.items[8] = items[1]; update(u); }; return html`
      ${items.map( (item, i) => html`
    • ${item.name} ${useCounts ? ` (${renders} renders)` : ''}
    • ` )}
    `; } const root = createRoot(document.body); let data = { items: new Array(1000).fill(null).map((x, i) => ({ name: `Item ${i + 1}` })), renders: 0, useKeys: false, useCounts: false }; function update(partial) { if (partial) Object.assign(data, partial); data.renders++; data.update = update; root.render(List(data)); } update(); ================================================ FILE: demo/logger.jsx ================================================ export default function logger(logStats, logConsole) { if (!logStats && !logConsole) { return; } const consoleBuffer = new ConsoleBuffer(); let calls = {}; let lock = true; function serialize(obj) { if (obj instanceof Text) return '#text'; if (obj instanceof Element) return `<${obj.localName}>`; if (obj === document) return 'document'; return Object.prototype.toString.call(obj).replace(/(^\[object |\]$)/g, ''); } function count(key) { if (lock === true) return; calls[key] = (calls[key] || 0) + 1; if (logConsole) { consoleBuffer.log(key); } } function logCall(obj, method, name) { let old = obj[method]; obj[method] = function () { let c = ''; for (let i = 0; i < arguments.length; i++) { if (c) c += ', '; c += serialize(arguments[i]); } count(`${serialize(this)}.${method}(${c})`); return old.apply(this, arguments); }; } logCall(document, 'createElement'); logCall(document, 'createElementNS'); logCall(Element.prototype, 'remove'); logCall(Element.prototype, 'appendChild'); logCall(Element.prototype, 'removeChild'); logCall(Element.prototype, 'insertBefore'); logCall(Element.prototype, 'replaceChild'); logCall(Element.prototype, 'setAttribute'); logCall(Element.prototype, 'setAttributeNS'); logCall(Element.prototype, 'removeAttribute'); logCall(Element.prototype, 'removeAttributeNS'); let d = Object.getOwnPropertyDescriptor(CharacterData.prototype, 'data') || Object.getOwnPropertyDescriptor(Node.prototype, 'data'); Object.defineProperty(Text.prototype, 'data', { get() { let value = d.get.call(this); count(`get #text.data`); return value; }, set(v) { count(`set #text.data`); return d.set.call(this, v); } }); let root; function setup() { if (!logStats) return; lock = true; root = document.createElement('table'); root.style.cssText = 'position: fixed; right: 0; top: 0; z-index:999; background: #000; font-size: 12px; color: #FFF; opacity: 0.9; white-space: nowrap;'; let header = document.createElement('thead'); header.innerHTML = '
    '; root.tableBody = document.createElement('tbody'); root.appendChild(root.tableBody); root.appendChild(header); document.documentElement.appendChild(root); let btn = document.getElementById('clear-logs'); btn.addEventListener('click', () => { for (let key in calls) { calls[key] = 0; } }); lock = false; } let rows = {}; function createRow(id) { let row = document.createElement('tr'); row.key = document.createElement('td'); row.key.textContent = id; row.appendChild(row.key); row.value = document.createElement('td'); row.value.textContent = ' '; row.appendChild(row.value); root.tableBody.appendChild(row); return (rows[id] = row); } function insertInto(parent) { parent.appendChild(root); } function remove() { clearInterval(updateTimer); } function update() { if (!logStats) return; lock = true; for (let i in calls) { if (calls.hasOwnProperty(i)) { let row = rows[i] || createRow(i); row.value.firstChild.nodeValue = calls[i]; } } lock = false; } let updateTimer = setInterval(update, 50); setup(); lock = false; return { insertInto, update, remove }; } /** * Logging to the console significantly affects performance. * Buffer calls to console and replay them at the end of the * current stack * @extends {Console} */ class ConsoleBuffer { constructor() { /** @type {Array<[string, any[]]>} */ this.buffer = []; this.deferred = null; for (let methodName of Object.keys(console)) { this[methodName] = this.proxy(methodName); } } proxy(methodName) { return (...args) => { this.buffer.push([methodName, args]); this.deferFlush(); }; } deferFlush() { if (this.deferred == null) { this.deferred = Promise.resolve() .then(() => this.flush()) .then(() => (this.deferred = null)); } } flush() { let method; while ((method = this.buffer.shift())) { let [name, args] = method; console[name](...args); } } } ================================================ FILE: demo/mobx.jsx ================================================ import React, { forwardRef, useRef, useState } from 'react'; import { decorate, observable } from 'mobx'; import { observer, useObserver } from 'mobx-react'; class Todo { constructor() { this.id = Math.random(); this.title = 'initial'; this.finished = false; } } decorate(Todo, { title: observable, finished: observable }); const Forward = observer( // eslint-disable-next-line react/display-name forwardRef(({ todo }, ref) => { return (

    Forward: "{todo.title}" {'' + todo.finished}

    ); }) ); const todo = new Todo(); const TodoView = observer(({ todo }) => { return (

    Todo View: "{todo.title}" {'' + todo.finished}

    ); }); const HookView = ({ todo }) => { return useObserver(() => { return (

    Todo View: "{todo.title}" {'' + todo.finished}

    ); }); }; export function MobXDemo() { const ref = useRef(null); let [v, set] = useState(0); const success = ref.current && ref.current.nodeName === 'P'; return (
    { todo.title = e.target.value; set(v + 1); }} />

    {success ? 'SUCCESS' : 'FAIL'}

    ); } ================================================ FILE: demo/nested-suspense/addnewcomponent.jsx ================================================ import { createElement } from 'react'; export default function AddNewComponent({ appearance }) { return
    AddNewComponent (component #{appearance})
    ; } ================================================ FILE: demo/nested-suspense/component-container.jsx ================================================ import { lazy } from 'react'; const pause = timeout => new Promise(d => setTimeout(d, timeout), console.log(timeout)); const SubComponent = lazy(() => pause(Math.random() * 1000).then(() => import('./subcomponent.jsx')) ); export default function ComponentContainer({ appearance }) { return (
    GenerateComponents (component #{appearance})
    ); } ================================================ FILE: demo/nested-suspense/dropzone.jsx ================================================ import { createElement } from 'react'; export default function DropZone({ appearance }) { return
    DropZone (component #{appearance})
    ; } ================================================ FILE: demo/nested-suspense/editor.jsx ================================================ import { createElement } from 'react'; export default function Editor({ children }) { return
    {children}
    ; } ================================================ FILE: demo/nested-suspense/index.jsx ================================================ import { createElement, Suspense, lazy, Component } from 'react'; const Loading = function () { return
    Loading...
    ; }; const Error = function ({ resetState }) { return (
    Error!  Reset app
    ); }; const pause = timeout => new Promise(d => setTimeout(d, timeout), console.log(timeout)); const DropZone = lazy(() => pause(Math.random() * 1000).then(() => import('./dropzone.jsx')) ); const Editor = lazy(() => pause(Math.random() * 1000).then(() => import('./editor.jsx')) ); const AddNewComponent = lazy(() => pause(Math.random() * 1000).then(() => import('./addnewcomponent.jsx')) ); const GenerateComponents = lazy(() => pause(Math.random() * 1000).then(() => import('./component-container.jsx')) ); export default class App extends Component { state = { hasError: false }; static getDerivedStateFromError(error) { // Update state so the next render will show the fallback UI. console.warn(error); return { hasError: true }; } render() { return this.state.hasError ? ( this.setState({ hasError: false })} /> ) : ( }>
    }>
    Footer here
    ); } } ================================================ FILE: demo/nested-suspense/subcomponent.jsx ================================================ import { createElement } from 'react'; export default function SubComponent({ onClick }) { return
    Lazy loaded sub component
    ; } ================================================ FILE: demo/old.js.bak ================================================ // function createRoot(title) { // let div = document.createElement('div'); // let h2 = document.createElement('h2'); // h2.textContent = title; // div.appendChild(h2); // document.body.appendChild(div); // return div; // } /* function logCall(obj, method, name) { let old = obj[method]; obj[method] = function(...args) { console.log(`<${this.localName}>.`+(name || `${method}(${args})`)); return old.apply(this, args); }; } logCall(HTMLElement.prototype, 'appendChild'); logCall(HTMLElement.prototype, 'removeChild'); logCall(HTMLElement.prototype, 'insertBefore'); logCall(HTMLElement.prototype, 'replaceChild'); logCall(HTMLElement.prototype, 'setAttribute'); logCall(HTMLElement.prototype, 'removeAttribute'); let d = Object.getOwnPropertyDescriptor(Node.prototype, 'nodeValue'); Object.defineProperty(Text.prototype, 'nodeValue', { get() { let value = d.get.call(this); console.log('get Text#nodeValue: ', value); return value; }, set(v) { console.log('set Text#nodeValue', v); return d.set.call(this, v); } }); render((

    This is a test.

    ), createRoot('Stateful component update demo:')); class Foo extends Component { componentDidMount() { console.log('mounted'); this.timer = setInterval( () => { this.setState({ time: Date.now() }); }, 5000); } componentWillUnmount() { clearInterval(this.timer); } render(props, state, context) { // console.log('rendering', props, state, context); return } } render((

    This is a test.

    ), createRoot('Stateful component update demo:')); let items = []; let count = 0; let three = createRoot('Top-level render demo:'); setInterval( () => { if (count++ %20 < 10 ) { items.push(
  • item #{items.length}
  • ); } else { items.shift(); } render((

    This is a test.

      {items}
    ), three); }, 5000); // Mount the top-level component to the DOM: render(
    , document.body); */ ================================================ FILE: demo/package.json ================================================ { "name": "demo", "main": "index.js", "scripts": { "start": "vite", "dev": "vite", "build": "vite build", "preview": "vite preview" }, "devDependencies": { "@babel/plugin-proposal-class-properties": "^7.0.0-beta.55", "@babel/plugin-proposal-decorators": "^7.4.0", "vite": "^5.4.10" }, "dependencies": { "@material-ui/core": "4.9.5", "@reduxjs/toolkit": "^2.2.3", "d3-scale": "^1.0.7", "d3-selection": "^1.2.0", "htm": "2.1.1", "mobx": "^5.15.4", "mobx-react": "^6.2.2", "mobx-state-tree": "^3.16.0", "preact-render-to-string": "^5.0.2", "preact-router": "^3.0.0", "react-redux": "^7.1.0", "react-router": "^5.0.1", "react-router-dom": "^5.0.1", "redux": "^4.0.1", "styled-components": "^4.2.0", "zustand": "^4.5.2" }, "volta": { "extends": "../package.json" } } ================================================ FILE: demo/people/Readme.md ================================================ # People demo page This section of our demo was originally made by [phaux](https://github.com/phaux) in the [web-app-boilerplate](https://github.com/phaux/web-app-boilerplate) repo. It has been slightly modified from it's original to better work inside of our demo app ================================================ FILE: demo/people/index.tsx ================================================ import { observer } from 'mobx-react'; import { Component } from 'preact'; import { Profile } from './profile'; import { Link, Route, Router } from './router'; import { store } from './store'; import './styles/index.scss'; @observer export default class App extends Component { componentDidMount() { store.loadUsers().catch(console.error); } render() { return (
    ); } } ================================================ FILE: demo/people/profile.tsx ================================================ import { computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import { Component } from 'preact'; import { RouteChildProps } from './router'; import { store } from './store'; export type ProfileProps = RouteChildProps; @observer export class Profile extends Component { @observable id = ''; @observable busy = false; componentDidMount() { this.id = this.props.route; } componentWillReceiveProps(props: ProfileProps) { this.id = props.route; } render() { const user = this.user; if (user == null) return null; return (

    {user.name.first} {user.name.last}

    {user.gender === 'female' ? '👩' : '👨'} {user.id}

    🖂 {user.email}

    ); } @computed get user() { return store.users.find(u => u.id === this.id); } remove = async () => { this.busy = true; await new Promise(cb => setTimeout(cb, 1500)); store.deleteUser(this.id); this.busy = false; }; } ================================================ FILE: demo/people/router.tsx ================================================ import { ComponentChild, ComponentFactory, createContext, FunctionalComponent, h, JSX } from 'preact'; import { useCallback, useContext, useEffect, useMemo, useState } from 'preact/hooks'; export type RouterData = { match: string[]; path: string[]; navigate(path: string): void; }; const RouterContext = createContext({ match: [], path: [], navigate() {} }); export const useRouter = () => useContext(RouterContext); const useLocation = (cb: () => void) => { useEffect(() => { window.addEventListener('popstate', cb); return () => { window.removeEventListener('popstate', cb); }; }, [cb]); }; export const Router: FunctionalComponent = props => { const [path, setPath] = useState(location.pathname); const update = useCallback(() => { setPath(location.pathname); }, [setPath]); useLocation(update); const navigate = useCallback( (path: string) => { history.pushState(null, '', path); update(); }, [update] ); const router = useMemo( () => ({ match: [], navigate, path: path.split('/').filter(Boolean) }), [navigate, path] ); return ; }; export type RouteChildProps = { route: string }; export type RouteProps = { component?: ComponentFactory; match: string; render?(route: string): ComponentChild; }; export const Route: FunctionalComponent = props => { const router = useRouter(); const [dir, ...subpath] = router.path; if (dir == null) return null; if (props.match !== '*' && dir !== props.match) return null; const children = useMemo(() => { if (props.component) return ; if (props.render) return props.render(dir); return props.children; }, [props.component, props.render, props.children, dir]); const innerRouter = useMemo( () => ({ ...router, match: [...router.match, dir], path: subpath }), [router.match, dir, subpath.join('/')] ); return ; }; export type LinkProps = JSX.HTMLAttributes & { active?: boolean | string; }; export const Link: FunctionalComponent = props => { const router = useRouter(); const classProps = [props.class, props.className]; const originalClasses = useMemo(() => { const classes = []; for (const prop of classProps) if (prop) classes.push(...prop.split(/\s+/)); return classes; }, classProps); const activeClass = useMemo(() => { if (!props.active || props.href == null) return undefined; const href = props.href.split('/').filter(Boolean); const path = props.href[0] === '/' ? [...router.match, ...router.path] : router.path; const isMatch = href.every((dir, i) => dir === path[i]); if (isMatch) return props.active === true ? 'active' : props.active; }, [originalClasses, props.active, props.href, router.match, router.path]); const classes = activeClass == null ? originalClasses : [...originalClasses, activeClass]; const getHref = useCallback(() => { if (props.href == null || props.href[0] === '/') return props.href; const path = props.href.split('/').filter(Boolean); return '/' + [...router.match, ...path].join('/'); }, [router.match, props.href]); const handleClick = useCallback( (ev: MouseEvent) => { const href = getHref(); if (props.onClick != null) props.onClick(ev); if (ev.defaultPrevented) return; if (href == null) return; if (ev.button !== 0) return; if (props.target != null && props.target !== '_self') return; if (ev.metaKey || ev.altKey || ev.ctrlKey || ev.shiftKey) return; ev.preventDefault(); router.navigate(href); }, [getHref, router.navigate, props.onClick, props.target] ); return ( ); }; ================================================ FILE: demo/people/store.ts ================================================ import { flow, Instance, types } from 'mobx-state-tree'; const cmp = (fn: (x: T) => U) => (a: T, b: T): number => fn(a) > fn(b) ? 1 : -1; const User = types.model({ email: types.string, gender: types.enumeration(['male', 'female']), id: types.identifier, name: types.model({ first: types.string, last: types.string }), picture: types.model({ large: types.string }) }); const Store = types .model({ users: types.array(User), usersOrder: types.enumeration(['name', 'id']) }) .views(self => ({ getSortedUsers() { if (self.usersOrder === 'name') return self.users.slice().sort(cmp(x => x.name.first)); if (self.usersOrder === 'id') return self.users.slice().sort(cmp(x => x.id)); throw Error(`Unknown ordering ${self.usersOrder}`); } })) .actions(self => ({ addUser: flow(function* () { const data = yield fetch('https://randomuser.me/api?results=1') .then(res => res.json()) .then(data => data.results.map((user: any) => ({ ...user, id: user.login.username })) ); self.users.push(...data); }), loadUsers: flow(function* () { const data = yield fetch( `https://randomuser.me/api?seed=${12321}&results=12` ) .then(res => res.json()) .then(data => data.results.map((user: any) => ({ ...user, id: user.login.username })) ); self.users.replace(data); }), deleteUser(id: string) { const user = self.users.find(u => u.id === id); if (user != null) self.users.remove(user); }, setUsersOrder(order: 'name' | 'id') { self.usersOrder = order; } })); export type StoreType = Instance; export const store = Store.create({ usersOrder: 'name', users: [] }); // const { Provider, Consumer } = createContext(undefined as any) // export const StoreProvider: FunctionalComponent = props => { // const store = Store.create({}) // return // } // export type StoreProps = {store: StoreType} // export function injectStore(Child: AnyComponent): FunctionalComponent { // return props => }/> // } ================================================ FILE: demo/people/styles/animations.scss ================================================ @keyframes popup { from { box-shadow: 0 0 0 black; opacity: 0; transform: scale(0.9); } to { box-shadow: 0 30px 70px rgba(0, 0, 0, 0.5); opacity: 1; transform: none; } } @keyframes zoom { from { opacity: 0; transform: scale(0.8); } to { opacity: 1; transform: none; } } @keyframes appear-from-left { from { opacity: 0; transform: translateX(-25px); } to { opacity: 1; transform: none; } } ================================================ FILE: demo/people/styles/app.scss ================================================ #people-app { position: relative; overflow: hidden; min-height: 100vh; animation: popup 300ms cubic-bezier(0.3, 0.7, 0.3, 1) forwards; background: var(--app-background); --menu-width: 260px; --menu-item-height: 50px; @media (min-width: 1280px) { max-width: 1280px; min-height: calc(100vh - 64px); margin: 32px auto; border-radius: 10px; } > nav { position: absolute; display: flow-root; width: var(--menu-width); height: 100%; background-color: var(--app-background-secondary); overflow-x: hidden; overflow-y: auto; } > nav h4 { padding-left: 16px; font-weight: normal; text-transform: uppercase; } > nav ul { position: relative; } > nav li { position: absolute; width: 100%; animation: zoom 200ms forwards; opacity: 0; transition: top 200ms; } > nav li > a { position: relative; display: flex; overflow: hidden; flex-flow: row; align-items: center; margin-left: 16px; border-right: 2px solid transparent; border-bottom-left-radius: 48px; border-top-left-radius: 48px; text-transform: capitalize; transition: border 500ms; } > nav li > a:hover { background-color: var(--app-highlight); } > nav li > a::after { position: absolute; top: 0; right: -2px; bottom: 0; left: 0; background-image: radial-gradient( circle, var(--app-ripple) 1%, transparent 1% ); background-position: center; background-repeat: no-repeat; background-size: 10000%; content: ''; opacity: 0; transition: opacity 700ms, background 300ms; } > nav li > a:active::after { background-size: 100%; opacity: 0.5; transition: none; } > nav li > a.active { border-color: var(--app-primary); background-color: var(--app-highlight); } > nav li > a > * { margin: 8px; } #people-main { padding-left: var(--menu-width); } } ================================================ FILE: demo/people/styles/avatar.scss ================================================ #people-app { .avatar { display: inline-block; overflow: hidden; width: var(--avatar-size, 32px); height: var(--avatar-size, 32px); background-color: var(--avatar-color, var(--app-primary)); border-radius: 50%; font-size: calc(var(--avatar-size, 32px) * 0.5); line-height: var(--avatar-size, 32px); object-fit: cover; text-align: center; text-transform: uppercase; white-space: nowrap; } } ================================================ FILE: demo/people/styles/button.scss ================================================ #people-app { button { position: relative; overflow: hidden; min-width: 36px; height: 36px; padding: 0 16px; border: none; background-color: transparent; border-radius: 4px; color: var(--app-text); font-family: 'Montserrat', sans-serif; font-size: 14px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; transition: background 300ms, color 200ms; white-space: nowrap; } button::before { position: absolute; top: 0; right: 0; bottom: 0; left: 0; background-color: var(--app-ripple); content: ''; opacity: 0; transition: opacity 200ms; } button:hover:not(:disabled)::before { opacity: 0.3; transition: opacity 100ms; } button:active:not(:disabled)::before { opacity: 0.7; transition: none; } button::after { position: absolute; top: 0; right: 0; bottom: 0; left: 0; background-image: radial-gradient( circle, var(--app-ripple) 1%, transparent 1% ); background-position: center; background-repeat: no-repeat; background-size: 20000%; content: ''; opacity: 0; transition: opacity 700ms, background 400ms; } button:active:not(:disabled)::after { background-size: 100%; opacity: 1; transition: none; } button.primary { background-color: var(--app-primary); box-shadow: 0 2px 6px var(--app-shadow); } button.secondary { background-color: var(--app-secondary); box-shadow: 0 2px 6px var(--app-shadow); } button:disabled { color: var(--app-text-secondary); } button.busy { animation: stripes 500ms linear infinite; background-image: repeating-linear-gradient( 45deg, var(--app-shadow) 0%, var(--app-shadow) 25%, transparent 25%, transparent 50%, var(--app-shadow) 50%, var(--app-shadow) 75%, transparent 75%, transparent 100% ); color: var(--app-text); /* letter-spacing: -.7em; */ } button:disabled:not(.primary):not(.secondary).busy, button:disabled.primary:not(.busy), button:disabled.secondary:not(.busy) { background-color: var(--app-background-disabled); } @keyframes stripes { from { background-position-x: 0; background-size: 16px 16px; } to { background-position-x: 16px; background-size: 16px 16px; } } } ================================================ FILE: demo/people/styles/index.scss ================================================ @import 'app.scss'; @import 'animations.scss'; @import 'avatar.scss'; @import 'profile.scss'; @import 'button.scss'; // :root { #people-app { --app-background: #2f2b43; --app-background-secondary: #353249; --app-background-disabled: #555366; --app-highlight: rgba(255, 255, 255, 0.1); --app-ripple: rgba(255, 255, 255, 0.5); --app-shadow: rgba(0, 0, 0, 0.15); --app-text: #fff; --app-text-secondary: #807e97; --app-primary: #ff0087; --app-secondary: #4d7cfe; --app-tertiary: #00ec97; --app-danger: #f3c835; --spinner-size: 200px; } * { box-sizing: border-box; } // body { #people-app { // display: flow-root; // overflow: auto; // min-height: 100vh; // margin: 0; // animation: background-light 5s ease-out forwards; // /* very fancy background */ // background: radial-gradient( // circle 15px at 150px 90vh, // rgba(255, 255, 255, 0.35), // rgba(255, 255, 255, 0.35) 90%, // transparent // ), // radial-gradient( // circle 9px at 60px 50vh, // rgba(255, 255, 255, 0.55), // rgba(255, 255, 255, 0.55) 90%, // transparent // ), // radial-gradient( // circle 19px at 40vw 70px, // rgba(255, 255, 255, 0.3), // rgba(255, 255, 255, 0.3) 90%, // transparent // ), // radial-gradient( // circle 12px at 80vw 80px, // rgba(255, 255, 255, 0.4), // rgba(255, 255, 255, 0.4) 90%, // transparent // ), // radial-gradient( // circle 7px at 55vw calc(100vh - 95px), // rgba(255, 255, 255, 0.6), // rgba(255, 255, 255, 0.6) 90%, // transparent // ), // radial-gradient( // circle 14px at 25vw calc(100vh - 35px), // rgba(255, 255, 255, 0.4), // rgba(255, 255, 255, 0.4) 90%, // transparent // ), // radial-gradient( // circle 11px at calc(100vw - 95px) 55vh, // rgba(255, 255, 255, 0.45), // rgba(255, 255, 255, 0.45) 90%, // transparent // ), // radial-gradient( // circle 13px at calc(100vw - 35px) 85vh, // rgba(255, 255, 255, 0.4), // rgba(255, 255, 255, 0.4) 90%, // transparent // ), // radial-gradient( // circle 50vw at 0 -25%, // rgba(255, 255, 255, 0.07), // rgba(255, 255, 255, 0.07) 100%, // transparent // ), // radial-gradient( // circle 80vw at top left, // rgba(255, 255, 255, 0.07), // rgba(255, 255, 255, 0.07) 100%, // transparent // ), // radial-gradient(circle at bottom right, #ef2fb8, transparent), // radial-gradient(circle at top right, #c45af3, transparent), // linear-gradient(#ee66ca, #ff47a6); color: var(--app-text); font-family: 'Montserrat', sans-serif; } #people-app { .spinner { position: absolute; top: 200px; left: calc(50% - var(--spinner-size) / 2); width: var(--spinner-size); height: var(--spinner-size); animation: zoom 250ms 500ms forwards ease-out; opacity: 0; transition: opacity 200ms, transform 200ms ease-in; } .spinner.exit { opacity: 0; transform: scale(0.5); } .spinner::before, .spinner::after { position: absolute; top: 0; left: 0; width: calc(var(--spinner-size) / 3); height: calc(var(--spinner-size) / 3); animation: spinner 2s infinite ease-in-out; background-color: rgba(255, 255, 255, 0.6); content: ''; } .spinner::after { animation-delay: -1s; } @keyframes spinner { 25% { transform: translateX(calc(var(--spinner-size) / 3 * 2 - 1px)) rotate(-90deg) scale(0.5); } 50% { transform: translateX(calc(var(--spinner-size) / 3 * 2 - 1px)) translateY(calc(var(--spinner-size) / 3 * 2 - 1px)) rotate(-179deg); } 50.1% { transform: translateX(calc(var(--spinner-size) / 3 * 2 - 1px)) translateY(calc(var(--spinner-size) / 3 * 2 - 1px)) rotate(-180deg); } 75% { transform: translateX(0) translateY(calc(var(--spinner-size) / 3 * 2 - 1px)) rotate(-270deg) scale(0.5); } 100% { transform: rotate(-360deg); } } ul, ol { padding-left: 0; list-style: none; } a { color: inherit; text-decoration: none; } } ================================================ FILE: demo/people/styles/profile.scss ================================================ #people-app { .profile { display: flex; flex-flow: column; align-items: center; margin: 32px 0; animation: appear-from-left 0.5s forwards; --avatar-size: 80px; } .profile h2 { text-transform: capitalize; } .profile .details { display: flex; flex-flow: column; align-items: stretch; margin: 16px auto; } .profile .details p { margin-top: 8px; margin-bottom: 8px; } } ================================================ FILE: demo/preact.jsx ================================================ import { options, createElement, cloneElement, Component as CevicheComponent, render } from 'preact'; options.vnode = vnode => { vnode.nodeName = vnode.type; vnode.attributes = vnode.props; vnode.children = vnode._children || [].concat(vnode.props.children || []); }; function asArray(arr) { return Array.isArray(arr) ? arr : [arr]; } function normalize(obj) { if (Array.isArray(obj)) { return obj.map(normalize); } if ('type' in obj && !('attributes' in obj)) { obj.attributes = obj.props; } return obj; } export function Component(props, context) { CevicheComponent.call(this, props, context); const render = this.render; this.render = function (props, state, context) { if (props.children) props.children = asArray(normalize(props.children)); return render.call(this, props, state, context); }; } Component.prototype = new CevicheComponent(); export { createElement, createElement as h, cloneElement, render }; ================================================ FILE: demo/profiler.jsx ================================================ import { createElement, Component, options } from 'preact'; function getPrimes(max) { let sieve = [], i, j, primes = []; for (i = 2; i <= max; ++i) { if (!sieve[i]) { // i has not been marked -- it is prime primes.push(i); for (j = i << 1; j <= max; j += i) { sieve[j] = true; } } } return primes.join(''); } function Foo(props) { return
    {props.children}
    ; } function Bar() { getPrimes(10000); return (
    ...yet another component
    ); } function PrimeNumber(props) { // Slow down rendering of this component getPrimes(10); return (
    I'm a slow component
    {props.children}
    ); } export default class ProfilerDemo extends Component { constructor() { super(); this.onClick = this.onClick.bind(this); this.state = { counter: 0 }; } componentDidMount() { options._diff = vnode => (vnode.startTime = performance.now()); options.diffed = vnode => (vnode.endTime = performance.now()); } componentWillUnmount() { delete options._diff; delete options.diffed; } onClick() { this.setState(prev => ({ counter: ++prev.counter })); } render() { return (

    ⚛ Preact

    Devtools Profiler integration 🕒

    I'm a fast component I'm the fastest component 🎉 Counter: {this.state.counter}

    ); } } ================================================ FILE: demo/pythagoras/index.jsx ================================================ import { Component } from 'preact'; import { select as d3select, mouse as d3mouse } from 'd3-selection'; import { scaleLinear } from 'd3-scale'; import Pythagoras from './pythagoras'; export default class PythagorasDemo extends Component { svg = { width: 1280, height: 600 }; state = { currentMax: 0, baseW: 80, heightFactor: 0, lean: 0 }; realMax = 11; svgRef = c => { this.svgElement = c; }; scaleFactor = scaleLinear().domain([this.svg.height, 0]).range([0, 0.8]); scaleLean = scaleLinear() .domain([0, this.svg.width / 2, this.svg.width]) .range([0.5, 0, -0.5]); onMouseMove = event => { let [x, y] = d3mouse(this.svgElement); this.setState({ heightFactor: this.scaleFactor(y), lean: this.scaleLean(x) }); }; restart = () => { this.setState({ currentMax: 0 }); this.next(); }; next = () => { let { currentMax } = this.state; if (currentMax < this.realMax) { this.setState({ currentMax: currentMax + 1 }); this.timer = setTimeout(this.next, 500); } }; componentDidMount() { this.selected = d3select(this.svgElement).on('mousemove', this.onMouseMove); this.next(); } componentWillUnmount() { this.selected.on('mousemove', null); clearTimeout(this.timer); } render({}, { currentMax, baseW, heightFactor, lean }) { let { width, height } = this.svg; return (
    ); } } ================================================ FILE: demo/pythagoras/pythagoras.jsx ================================================ import { interpolateViridis } from 'd3-scale'; Math.deg = function (radians) { return radians * (180 / Math.PI); }; const memoizedCalc = (function () { const memo = {}; const key = ({ w, heightFactor, lean }) => `${w}-${heightFactor}-${lean}`; return args => { let memoKey = key(args); if (memo[memoKey]) { return memo[memoKey]; } let { w, heightFactor, lean } = args; let trigH = heightFactor * w; let result = { nextRight: Math.sqrt(trigH ** 2 + (w * (0.5 + lean)) ** 2), nextLeft: Math.sqrt(trigH ** 2 + (w * (0.5 - lean)) ** 2), A: Math.deg(Math.atan(trigH / ((0.5 - lean) * w))), B: Math.deg(Math.atan(trigH / ((0.5 + lean) * w))) }; memo[memoKey] = result; return result; }; })(); export default function Pythagoras({ w, x, y, heightFactor, lean, left, right, lvl, maxlvl }) { if (lvl >= maxlvl || w < 1) { return null; } const { nextRight, nextLeft, A, B } = memoizedCalc({ w, heightFactor, lean }); let rotate = ''; if (left) { rotate = `rotate(${-A} 0 ${w})`; } else if (right) { rotate = `rotate(${B} ${w} ${w})`; } return ( ); } ================================================ FILE: demo/redux-toolkit.jsx ================================================ import { createElement } from 'preact'; import { Provider, useSelector } from 'react-redux'; import { configureStore, createSlice } from '@reduxjs/toolkit'; const initialState = { value: 0 }; const counterSlice = createSlice({ name: 'counter', initialState, reducers: { increment: state => { state.value += 1; }, decrement: state => { state.value -= 1; } } }); const store = configureStore({ reducer: { counter: counterSlice.reducer } }); function Counter({ number }) { const count = useSelector(state => state.counter.value); return (
    Counter #{number}:{count}
    ); } export default function ReduxToolkit() { function increment() { store.dispatch(counterSlice.actions.increment()); } function decrement() { store.dispatch(counterSlice.actions.decrement()); } function incrementAsync() { setTimeout(() => { store.dispatch(counterSlice.actions.increment()); }, 1000); } return (

    Redux Toolkit

    Counter

    ); } ================================================ FILE: demo/redux.jsx ================================================ import { createElement } from 'preact'; import React from 'react'; import { createStore } from 'redux'; import { connect, Provider } from 'react-redux'; const store = createStore((state = { value: 0 }, action) => { switch (action.type) { case 'increment': return { value: state.value + 1 }; case 'decrement': return { value: state.value - 1 }; default: return state; } }); class Child extends React.Component { render() { return (
    Child #1: {this.props.foo}
    ); } } const ConnectedChild = connect(store => ({ foo: store.value }))(Child); class Child2 extends React.Component { render() { return
    Child #2: {this.props.foo}
    ; } } const ConnectedChild2 = connect(store => ({ foo: store.value }))(Child2); export default function Redux() { return (

    Counter


    ); } ================================================ FILE: demo/reduxUpdate.jsx ================================================ import { createElement, Component } from 'preact'; import { connect, Provider } from 'react-redux'; import { createStore } from 'redux'; import { HashRouter, Route, Link } from 'react-router-dom'; const store = createStore( (state, action) => ({ ...state, display: action.display }), { display: false } ); function _Redux({ showMe, counter }) { if (!showMe) return null; return
    showMe {counter}
    ; } const Redux = connect( state => console.log('injecting', state.display) || { showMe: state.display } )(_Redux); let display = false; class Test extends Component { componentDidUpdate(prevProps) { if (this.props.start != prevProps.start) { this.setState({ f: (this.props.start || 0) + 1 }); setTimeout(() => this.setState({ i: (this.state.i || 0) + 1 })); } } render() { const { f } = this.state; return (
    Click me
    ); } } function App() { return ( } /> ); } export default App; ================================================ FILE: demo/reorder.jsx ================================================ import { createElement, Component } from 'preact'; function createItems(count = 10) { let items = []; for (let i = 0; i < count; i++) { items.push({ label: `Item #${i + 1}`, key: i + 1 }); } return items; } function random() { return Math.random() < 0.5 ? 1 : -1; } export default class Reorder extends Component { state = { items: createItems(), count: 1, useKeys: false }; shuffle = () => { this.setState({ items: this.state.items.slice().sort(random) }); }; swapTwo = () => { let items = this.state.items.slice(), first = Math.floor(Math.random() * items.length), second; do { second = Math.floor(Math.random() * items.length); } while (second === first); let other = items[first]; items[first] = items[second]; items[second] = other; this.setState({ items }); }; reverse = () => { this.setState({ items: this.state.items.slice().reverse() }); }; setCount = e => { this.setState({ count: Math.round(e.target.value) }); }; rotate = () => { let { items, count } = this.state; items = items.slice(count).concat(items.slice(0, count)); this.setState({ items }); }; rotateBackward = () => { let { items, count } = this.state, len = items.length; items = items.slice(len - count, len).concat(items.slice(0, len - count)); this.setState({ items }); }; toggleKeys = () => { this.setState({ useKeys: !this.state.useKeys }); }; renderItem = item => (
  • {item.label}
  • ); render({}, { items, count, useKeys }) { return (
      {items.map(this.renderItem)}
    ); } } ================================================ FILE: demo/spiral.jsx ================================================ import { createElement, Component } from 'preact'; const COUNT = 500; const LOOPS = 6; // Component.debounce = requestAnimationFrame; export default class Spiral extends Component { state = { x: 0, y: 0, big: false, counter: 0 }; handleClick = e => { console.log('click'); }; increment = () => { if (this.stop) return; // this.setState({ counter: this.state.counter + 1 }, this.increment); this.setState({ counter: this.state.counter + 1 }); // this.forceUpdate(); requestAnimationFrame(this.increment); }; setMouse({ pageX: x, pageY: y }) { this.setState({ x, y }); return false; } setBig(big) { this.setState({ big }); } componentDidMount() { console.log('mount'); // let touch = navigator.maxTouchPoints > 1; let touch = false; // set mouse position state on move: addEventListener(touch ? 'touchmove' : 'mousemove', e => { this.setMouse(e.touches ? e.touches[0] : e); }); // holding the mouse down enables big mode: addEventListener(touch ? 'touchstart' : 'mousedown', e => { this.setBig(true); e.preventDefault(); }); addEventListener(touch ? 'touchend' : 'mouseup', e => this.setBig(false)); requestAnimationFrame(this.increment); } componentWillUnmount() { console.log('unmount'); this.stop = true; } // componentDidUpdate() { // // invoking setState() in componentDidUpdate() creates an animation loop: // this.increment(); // } // builds and returns a brand new DOM (every time) render(props, { x, y, big, counter }) { let max = COUNT + Math.round(Math.sin((counter / 90) * 2 * Math.PI) * COUNT * 0.5), cursors = []; // the advantage of JSX is that you can use the entirety of JS to "template": for (let i = max; i--; ) { let f = (i / max) * LOOPS, θ = f * 2 * Math.PI, m = 20 + i * 2, hue = (f * 255 + counter * 10) % 255; cursors[i] = ( ); } return (
    {cursors}
    ); } } /** Represents a single coloured dot. */ class Cursor extends Component { // get shared/pooled class object getClass(big, label) { let cl = 'cursor'; if (big) cl += ' big'; if (label) cl += ' label'; return cl; } // skip any pointless re-renders shouldComponentUpdate(props) { for (let i in props) if (i !== 'children' && props[i] !== this.props[i]) return true; return false; } // first argument is "props", the attributes passed to render({ x, y, label, color, big }) { let inner = null; if (label) inner = ( {x},{y} ); return (
    {inner}
    ); } } // Addendum: disable dragging on mobile addEventListener('touchstart', e => (e.preventDefault(), false)); ================================================ FILE: demo/stateOrderBug.jsx ================================================ import htm from 'htm'; import { h } from 'preact'; import { useState, useCallback } from 'preact/hooks'; const html = htm.bind(h); // configuration used to show behavior vs. workaround let childFirst = true; const Config = () => html` `; const Child = ({ items, setItems }) => { let [pendingId, setPendingId] = useState(null); if (!pendingId) { setPendingId((pendingId = Math.random().toFixed(20).slice(2))); } const onInput = useCallback( evt => { let val = evt.target.value, _items = [...items, { _id: pendingId, val }]; if (childFirst) { setPendingId(null); setItems(_items); } else { setItems(_items); setPendingId(null); } }, [childFirst, setPendingId, setItems, items, pendingId] ); return html`
    ${items.map( (item, idx) => html` { let val = evt.target.value, _items = [...items]; _items.splice(idx, 1, { ...item, val }); setItems(_items); }} /> ` )}
    `; }; const Parent = () => { let [items, setItems] = useState([]); return html`
    <${Config} /><${Child} items=${items} setItems=${setItems} />
    `; }; export default Parent; ================================================ FILE: demo/style.css ================================================ html, body { font: 14px system-ui, sans-serif; } .list { list-style: none; padding: 0; } .list > li { position: relative; padding: 5px 10px; animation: fadeIn 1s ease; } @keyframes fadeIn { 0% { box-shadow: inset 0 0 2px 2px red, 0 0 2px 2px red; } } .list > .odd { background-color: #def; } .list > .even { background-color: #fed; } ================================================ FILE: demo/style.scss ================================================ html, body { height: 100%; margin: 0; background: #eee; font: 400 16px/1.3 'Helvetica Neue', helvetica, sans-serif; text-rendering: optimizeSpeed; color: #444; } .app { display: block; flex-direction: column; height: 100%; > header { flex: 0; background: #f9f9f9; box-shadow: inset 0 -0.5px 0 0 rgba(0, 0, 0, 0.2), 0 0.5px 0 0 rgba(255, 255, 255, 0.6); nav { display: inline-block; padding: 4px 7px; a { display: inline-block; margin: 2px; padding: 4px 10px; background-color: rgba(255, 255, 255, 0); border-radius: 1em; color: #6b1d8f; text-decoration: none; // transition: all 250ms ease; transition: all 250ms cubic-bezier(0.2, 0, 0.4, 2); &:hover { background-color: rgba(255, 255, 255, 1); box-shadow: 0 0 0 2px #6b1d8f; } &.active { background-color: #6b1d8f; color: white; } } } } > main { flex: 1; padding: 10px; } } h1 { margin: 0; color: #6b1d8f; font-weight: 300; font-size: 250%; } input, textarea { box-sizing: border-box; margin: 1px; padding: 0.25em 0.5em; background: #fff; border: 1px solid #999; border-radius: 3px; font: inherit; color: #000; outline: none; &:focus { border-color: #6b1d8f; } } button, input[type='submit'], input[type='reset'], input[type='button'] { box-sizing: border-box; margin: 1px; padding: 0.25em 0.8em; background: #6b1d8f; border: 1px solid #6b1d8f; // border: none; border-radius: 1.5em; font: inherit; color: white; outline: none; cursor: pointer; } .cursor { position: absolute; left: 0; top: 0; width: 8px; height: 8px; margin: -5px 0 0 -5px; border: 2px solid #f00; border-radius: 50%; transform-origin: 50% 50%; pointer-events: none; overflow: hidden; font-size: 9px; line-height: 25px; text-indent: 15px; white-space: nowrap; &:not(.label) { contain: strict; } &.label { overflow: visible; } // &.big { // transform: scale(2); // // width: 24px; // // height: 24px; // // margin: -13px 0 0 -13px; // } .label { position: absolute; left: 0; top: 0; //transform: translateZ(0); // z-index: 10; } } .animation-picker { position: fixed; display: inline-block; right: 0; top: 0; padding: 10px; background: #000; color: #bbb; z-index: 1000; select { font-size: 100%; margin-left: 5px; } } ================================================ FILE: demo/styled-components.jsx ================================================ import { createElement } from 'preact'; import styled, { css } from 'styled-components'; const Button = styled.button` background: transparent; border-radius: 3px; border: 2px solid palevioletred; color: palevioletred; margin: 0.5em 1em; padding: 0.25em 1em; ${props => props.primary && css` background: palevioletred; color: white; `} `; const Container = styled.div` text-align: center; `; export default function StyledComp() { return ( ); } ================================================ FILE: demo/suspense-router/bye.jsx ================================================ import { Link } from './simple-router'; export default function Bye() { return (
    Bye! Go to Hello!
    ); } ================================================ FILE: demo/suspense-router/hello.jsx ================================================ import { Link } from './simple-router'; export default function Hello() { return (
    Hello! Go to Bye!
    ); } ================================================ FILE: demo/suspense-router/index.jsx ================================================ import { Suspense, lazy } from 'react'; import { Router, Route, Switch } from './simple-router'; let Hello = lazy(() => import('./hello.jsx')); let Bye = lazy(() => import('./bye.jsx')); function Loading() { return
    Hey! This is a fallback because we're loading things! :D
    ; } export default function SuspenseRouterBug() { return (

    Suspense Router bug

    }>
    ); } ================================================ FILE: demo/suspense-router/simple-router.jsx ================================================ import { createContext, useState, useContext, Children, useLayoutEffect } from 'react'; const memoryHistory = { /** * @typedef {{ pathname: string }} Location * @typedef {(location: Location) => void} HistoryListener * @type {HistoryListener[]} */ listeners: [], /** * @param {HistoryListener} listener */ listen(listener) { const newLength = this.listeners.push(listener); return () => this.listeners.splice(newLength - 1, 1); }, /** * @param {Location} to */ navigate(to) { this.listeners.forEach(listener => listener(to)); } }; /** @type {import('react').Context<{ history: typeof memoryHistory; location: Location }>} */ const RouterContext = createContext(null); export function Router({ history = memoryHistory, children }) { const [location, setLocation] = useState({ pathname: '/' }); useLayoutEffect(() => { return history.listen(newLocation => setLocation(newLocation)); }, []); return ( {children} ); } export function Switch(props) { const { location } = useContext(RouterContext); let element = null; Children.forEach(props.children, child => { if (element == null && child.props.path == location.pathname) { element = child; } }); return element; } /** * @param {{ children: any; path: string; exact?: boolean; }} props */ export function Route({ children, path, exact }) { return children; } export function Link({ to, children }) { const { history } = useContext(RouterContext); const onClick = event => { event.preventDefault(); event.stopPropagation(); history.navigate({ pathname: to }); }; return (
    {children} ); } ================================================ FILE: demo/suspense.jsx ================================================ // eslint-disable-next-line no-unused-vars import { createElement, Component, memo, Fragment, Suspense, lazy } from 'react'; function LazyComp() { return
    I'm (fake) lazy loaded
    ; } const Lazy = lazy(() => Promise.resolve({ default: LazyComp })); function createSuspension(name, timeout, error) { let done = false; let prom; return { name, timeout, start: () => { if (!prom) { prom = new Promise((res, rej) => { setTimeout(() => { done = true; if (error) { rej(error); } else { res(); } }, timeout); }); } return prom; }, getPromise: () => prom, isDone: () => done }; } function CustomSuspense({ isDone, start, timeout, name }) { if (!isDone()) { throw start(); } return (
    Hello from CustomSuspense {name}, loaded after {timeout / 1000}s
    ); } function init() { return { s1: createSuspension('1', 1000, null), s2: createSuspension('2', 2000, null), s3: createSuspension('3', 3000, null) }; } export default class DevtoolsDemo extends Component { constructor(props) { super(props); this.state = init(); this.onRerun = this.onRerun.bind(this); } onRerun() { this.setState(init()); } render(props, state) { return (

    lazy()

    Loading (fake) lazy loaded component...
    }>

    Suspense

    Fallback 1}> Fallback 2}> ); } } ================================================ FILE: demo/textFields.jsx ================================================ import React, { useState } from 'react'; import TextField from '@material-ui/core/TextField'; const PatchedTextField = props => { const [value, set] = useState(props.value); return ( set(e.target.value)} /> ); }; const TextFields = () => (
    ); export default TextFields; ================================================ FILE: demo/todo.jsx ================================================ import { createElement, Component } from 'preact'; let counter = 0; export default class TodoList extends Component { state = { todos: [], text: '' }; setText = e => { this.setState({ text: e.target.value }); }; addTodo = () => { let { todos, text } = this.state; todos = todos.concat({ text, id: ++counter }); this.setState({ todos, text: '' }); }; removeTodo = e => { let id = e.target.getAttribute('data-id'); this.setState({ todos: this.state.todos.filter(t => t.id != id) }); }; render({}, { todos, text }) { return (
    ); } } class TodoItems extends Component { render({ todos, removeTodo }) { return todos.map(todo => (
  • {' '} {todo.text}
  • )); } } ================================================ FILE: demo/tsconfig.json ================================================ { "compilerOptions": { "experimentalDecorators": true, "jsx": "react", "jsxFactory": "h", "baseUrl": ".", "target": "es2018", "module": "es2015", "moduleResolution": "node", "paths": { "preact/hooks": ["../hooks/src/index.js"], "preact": ["../src/index.js"] } } } ================================================ FILE: demo/vite.config.js ================================================ import { defineConfig } from 'vite'; import path from 'node:path'; const root = path.join(__dirname, '..'); const resolvePkg = (...parts) => path.join(root, ...parts, 'src', 'index.js'); // https://vitejs.dev/config/ /** @type {import('vite').UserConfig} */ export default defineConfig({ optimizeDeps: { exclude: [ 'preact', 'preact/compat', 'preact/debug', 'preact/hooks', 'preact/devtools', 'preact/jsx-runtime', 'preact/jsx-dev-runtime', 'preact-router', 'react', 'react-dom' ] }, resolve: { alias: { 'preact/debug/src/debug': path.join(root, 'debug', 'src', 'debug'), 'preact/devtools/src/devtools': path.join( root, 'devtools', 'src', 'devtools' ), //'preact/debug': resolvePkg('debug'), 'preact/devtools': resolvePkg('devtools'), 'preact/hooks': resolvePkg('hooks'), 'preact/jsx-runtime': resolvePkg('jsx-runtime'), 'preact/jsx-dev-runtime': resolvePkg('jsx-runtime'), preact: resolvePkg(''), 'react-dom': resolvePkg('compat'), react: resolvePkg('compat') } }, esbuild: { jsx: 'automatic', jsxImportSource: 'preact' } }); ================================================ FILE: demo/zustand.jsx ================================================ import { createElement } from 'preact'; import create from 'zustand'; const useStore = create(set => ({ value: 0, text: 'John', setText: text => set(state => ({ ...state, text })), increment: () => set(state => ({ value: state.value + 1 })), decrement: () => set(state => ({ value: state.value - 1 })), incrementAsync: async () => { await new Promise(resolve => setTimeout(resolve, 1000)); set(state => ({ value: state.value + 1 })); } })); function Counter({ number }) { const value = useStore(state => state.value); return (
    Counter #{number}: {value}
    ); } function Text() { const text = useStore(state => state.text); const { setText } = useStore(); function handleInput(e) { setText(e.target.value); } return (
    Text: {text}
    ); } export default function ZustandComponent() { const { increment, decrement, incrementAsync } = useStore(); return (

    Zustand

    Counter

    ); } ================================================ FILE: devtools/LICENSE ================================================ The MIT License (MIT) Copyright (c) 2015-present Jason Miller Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: devtools/mangle.json ================================================ { "help": { "what is this file?": "It controls protected/private property mangling so that minified builds have consistent property names.", "why are there duplicate minified properties?": "Most properties are only used on one type of objects, so they can have the same name since they will never collide. Doing this reduces size." }, "minify": { "mangle": { "properties": { "regex": "^_[^_]", "reserved": [ "__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED", "__REACT_DEVTOOLS_GLOBAL_HOOK__", "__PREACT_DEVTOOLS__", "_renderers", "__source", "__self" ] } } } } ================================================ FILE: devtools/package.json ================================================ { "name": "preact-devtools", "amdName": "preactDevtools", "private": true, "description": "Preact bridge for Preact devtools", "main": "dist/devtools.js", "module": "dist/devtools.mjs", "umd:main": "dist/devtools.umd.js", "source": "src/index.js", "types": "src/index.d.ts", "license": "MIT", "peerDependencies": { "preact": "^10.0.0" } } ================================================ FILE: devtools/src/devtools.js ================================================ import { Component, Fragment, options } from 'preact'; export function initDevTools() { const globalVar = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : undefined; if ( globalVar !== null && globalVar !== undefined && globalVar.__PREACT_DEVTOOLS__ ) { globalVar.__PREACT_DEVTOOLS__.attachPreact('11.0.0-beta.1', options, { Fragment, Component }); } } ================================================ FILE: devtools/src/index.d.ts ================================================ /** * Customize the displayed name of a useState, useReducer or useRef hook * in the devtools panel. * * @param value Wrapped native hook. * @param name Custom name */ export function addHookName(value: T, name: string): T; ================================================ FILE: devtools/src/index.js ================================================ import { options } from 'preact'; import { initDevTools } from './devtools'; initDevTools(); /** * Display a custom label for a custom hook for the devtools panel * @type {(value: T, name: string) => T} */ export function addHookName(value, name) { if (options._addHookName) { options._addHookName(name); } return value; } ================================================ FILE: devtools/test/browser/addHookName.test.jsx ================================================ import { createElement, render, options } from 'preact'; import { setupScratch, teardown } from '../../../test/_util/helpers'; import { useState } from 'preact/hooks'; import { addHookName } from 'preact/devtools'; import { vi } from 'vitest'; describe('addHookName', () => { /** @type {HTMLDivElement} */ let scratch; beforeEach(() => { scratch = setupScratch(); }); afterEach(() => { teardown(scratch); delete options._addHookName; }); it('should do nothing when no options hook is present', () => { function useFoo() { return addHookName(useState(0), 'foo'); } function App() { let [v] = useFoo(); return
    {v}
    ; } expect(() => render(, scratch)).to.not.throw(); }); it('should call options hook with value', () => { let spy = (options._addHookName = vi.fn()); function useFoo() { return addHookName(useState(0), 'foo'); } function App() { let [v] = useFoo(); return
    {v}
    ; } render(, scratch); expect(spy).toHaveBeenCalledOnce(); expect(spy).toHaveBeenCalledWith('foo'); }); }); ================================================ FILE: hooks/LICENSE ================================================ The MIT License (MIT) Copyright (c) 2015-present Jason Miller Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: hooks/mangle.json ================================================ { "help": { "what is this file?": "It controls protected/private property mangling so that minified builds have consistent property names.", "why are there duplicate minified properties?": "Most properties are only used on one type of objects, so they can have the same name since they will never collide. Doing this reduces size." }, "minify": { "mangle": { "properties": { "regex": "^_[^_]", "reserved": [ "__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED", "__REACT_DEVTOOLS_GLOBAL_HOOK__", "__PREACT_DEVTOOLS__", "_renderers", "__source", "__self" ] } } } } ================================================ FILE: hooks/package.json ================================================ { "name": "preact-hooks", "amdName": "preactHooks", "private": true, "description": "Hook addon for Preact", "main": "dist/hooks.js", "module": "dist/hooks.mjs", "umd:main": "dist/hooks.umd.js", "source": "src/index.js", "types": "src/index.d.ts", "license": "MIT", "mangle": { "regex": "^_" }, "peerDependencies": { "preact": "^10.0.0" } } ================================================ FILE: hooks/src/index.d.ts ================================================ import { ErrorInfo, PreactContext, Ref, RefObject } from '../../src/index'; type Inputs = ReadonlyArray; export type Dispatch = (value: A) => void; export type StateUpdater = S | ((prevState: S) => S); /** * Returns a stateful value, and a function to update it. * @param initialState The initial value (or a function that returns the initial value) */ export function useState( initialState: S | (() => S) ): [S, Dispatch>]; export function useState(): [ S | undefined, Dispatch> ]; export type Reducer = (prevState: S, action: A) => S; /** * An alternative to `useState`. * * `useReducer` is usually preferable to `useState` when you have complex state logic that involves * multiple sub-values. It also lets you optimize performance for components that trigger deep * updates because you can pass `dispatch` down instead of callbacks. * @param reducer Given the current state and an action, returns the new state * @param initialState The initial value to store as state */ export function useReducer( reducer: Reducer, initialState: S ): [S, Dispatch]; /** * An alternative to `useState`. * * `useReducer` is usually preferable to `useState` when you have complex state logic that involves * multiple sub-values. It also lets you optimize performance for components that trigger deep * updates because you can pass `dispatch` down instead of callbacks. * @param reducer Given the current state and an action, returns the new state * @param initialArg The initial argument to pass to the `init` function * @param init A function that, given the `initialArg`, returns the initial value to store as state */ export function useReducer( reducer: Reducer, initialArg: I, init: (arg: I) => S ): [S, Dispatch]; /** * `useRef` returns a mutable ref object whose `.current` property is initialized to the passed argument * (`initialValue`). The returned object will persist for the full lifetime of the component. * * Note that `useRef()` is useful for more than the `ref` attribute. It’s handy for keeping any mutable * value around similar to how you’d use instance fields in classes. * * @param initialValue the initial value to store in the ref object */ export function useRef(initialValue: T): RefObject; export function useRef(initialValue: T | null): RefObject; export function useRef( initialValue: T | undefined ): RefObject; type EffectCallback = () => void | (() => void); /** * Accepts a function that contains imperative, possibly effectful code. * The effects run after browser paint, without blocking it. * * @param effect Imperative function that can return a cleanup function * @param inputs If present, effect will only activate if the values in the list change (using ===). */ export function useEffect(effect: EffectCallback, inputs?: Inputs): void; type CreateHandle = () => object; /** * @param ref The ref that will be mutated * @param create The function that will be executed to get the value that will be attached to * ref.current * @param inputs If present, effect will only activate if the values in the list change (using ===). */ export function useImperativeHandle( ref: Ref, create: () => R, inputs?: Inputs ): void; /** * Accepts a function that contains imperative, possibly effectful code. * Use this to read layout from the DOM and synchronously re-render. * Updates scheduled inside `useLayoutEffect` will be flushed synchronously, after all DOM mutations but before the browser has a chance to paint. * Prefer the standard `useEffect` hook when possible to avoid blocking visual updates. * * @param effect Imperative function that can return a cleanup function * @param inputs If present, effect will only activate if the values in the list change (using ===). */ export function useLayoutEffect(effect: EffectCallback, inputs?: Inputs): void; /** * Returns a memoized version of the callback that only changes if one of the `inputs` * has changed (using ===). */ export function useCallback(callback: T, inputs: Inputs): T; /** * Pass a factory function and an array of inputs. * useMemo will only recompute the memoized value when one of the inputs has changed. * This optimization helps to avoid expensive calculations on every render. * If no array is provided, a new value will be computed whenever a new function instance is passed as the first argument. */ // for `inputs`, allow undefined, but don't make it optional as that is very likely a mistake export function useMemo(factory: () => T, inputs: Inputs | undefined): T; /** * Returns the current context value, as given by the nearest context provider for the given context. * When the provider updates, this Hook will trigger a rerender with the latest context value. * * @param context The context you want to use */ export function useContext(context: PreactContext): T; /** * Customize the displayed value in the devtools panel. * * @param value Custom hook name or object that is passed to formatter * @param formatter Formatter to modify value before sending it to the devtools */ export function useDebugValue(value: T, formatter?: (value: T) => any): void; export function useErrorBoundary( callback?: (error: any, errorInfo: ErrorInfo) => Promise | void ): [any, () => void]; export function useId(): string; ================================================ FILE: hooks/src/index.js ================================================ import { options as _options } from 'preact'; import { COMPONENT_FORCE } from '../../src/constants'; const ObjectIs = Object.is; /** @type {number} */ let currentIndex; /** @type {import('./internal').Component} */ let currentComponent; /** @type {import('./internal').Component} */ let previousComponent; /** @type {number} */ let currentHook = 0; /** @type {Array} */ let afterPaintEffects = []; // Cast to use internal Options type const options = /** @type {import('./internal').Options} */ (_options); let oldBeforeDiff = options._diff; let oldBeforeRender = options._render; let oldAfterDiff = options.diffed; let oldCommit = options._commit; let oldBeforeUnmount = options.unmount; let oldRoot = options._root; // We take the minimum timeout for requestAnimationFrame to ensure that // the callback is invoked after the next frame. 35ms is based on a 30hz // refresh rate, which is the minimum rate for a smooth user experience. const RAF_TIMEOUT = 35; let prevRaf; /** @type {(vnode: import('./internal').VNode) => void} */ options._diff = vnode => { currentComponent = null; if (oldBeforeDiff) oldBeforeDiff(vnode); }; options._root = (vnode, parentDom) => { if (vnode && parentDom._children && parentDom._children._mask) { vnode._mask = parentDom._children._mask; } if (oldRoot) oldRoot(vnode, parentDom); }; /** @type {(vnode: import('./internal').VNode) => void} */ options._render = vnode => { if (oldBeforeRender) oldBeforeRender(vnode); currentComponent = vnode._component; currentIndex = 0; const hooks = currentComponent.__hooks; if (hooks) { if (previousComponent === currentComponent) { hooks._pendingEffects = []; currentComponent._renderCallbacks = []; hooks._list.some(hookItem => { if (hookItem._nextValue) { hookItem._value = hookItem._nextValue; } hookItem._pendingArgs = hookItem._nextValue = undefined; }); } else { hooks._pendingEffects.some(invokeCleanup); hooks._pendingEffects.some(invokeEffect); hooks._pendingEffects = []; currentIndex = 0; } } previousComponent = currentComponent; }; /** @type {(vnode: import('./internal').VNode) => void} */ options.diffed = vnode => { if (oldAfterDiff) oldAfterDiff(vnode); const c = vnode._component; if (c && c.__hooks) { if (c.__hooks._pendingEffects.length) afterPaint(afterPaintEffects.push(c)); c.__hooks._list.some(hookItem => { if (hookItem._pendingArgs) { hookItem._args = hookItem._pendingArgs; } hookItem._pendingArgs = undefined; }); } previousComponent = currentComponent = null; }; // TODO: Improve typing of commitQueue parameter /** @type {(vnode: import('./internal').VNode, commitQueue: any) => void} */ options._commit = (vnode, commitQueue) => { commitQueue.some(component => { try { component._renderCallbacks.some(invokeCleanup); component._renderCallbacks = component._renderCallbacks.filter(cb => cb._value ? invokeEffect(cb) : true ); } catch (e) { commitQueue.some(c => { if (c._renderCallbacks) c._renderCallbacks = []; }); commitQueue = []; options._catchError(e, component._vnode); } }); if (oldCommit) oldCommit(vnode, commitQueue); }; /** @type {(vnode: import('./internal').VNode) => void} */ options.unmount = vnode => { if (oldBeforeUnmount) oldBeforeUnmount(vnode); const c = vnode._component; if (c && c.__hooks) { let hasErrored; c.__hooks._list.some(s => { try { invokeCleanup(s); } catch (e) { hasErrored = e; } }); c.__hooks = undefined; if (hasErrored) options._catchError(hasErrored, c._vnode); } }; /** * Get a hook's state from the currentComponent * @param {number} index The index of the hook to get * @param {number} type The index of the hook to get * @returns {any} */ function getHookState(index, type) { if (options._hook) { options._hook(currentComponent, index, currentHook || type); } currentHook = 0; // Largely inspired by: // * https://github.com/michael-klein/funcy.js/blob/f6be73468e6ec46b0ff5aa3cc4c9baf72a29025a/src/hooks/core_hooks.mjs // * https://github.com/michael-klein/funcy.js/blob/650beaa58c43c33a74820a3c98b3c7079cf2e333/src/renderer.mjs // Other implementations to look at: // * https://codesandbox.io/s/mnox05qp8 const hooks = currentComponent.__hooks || (currentComponent.__hooks = { _list: [], _pendingEffects: [] }); if (index >= hooks._list.length) { hooks._list.push({}); } return hooks._list[index]; } /** * @template {unknown} S * @param {import('./index').Dispatch>} [initialState] * @returns {[S, (state: S) => void]} */ export function useState(initialState) { currentHook = 1; return useReducer(invokeOrReturn, initialState); } /** * @template {unknown} S * @template {unknown} A * @param {import('./index').Reducer} reducer * @param {import('./index').Dispatch>} initialState * @param {(initialState: any) => void} [init] * @returns {[ S, (state: S) => void ]} */ export function useReducer(reducer, initialState, init) { /** @type {import('./internal').ReducerHookState} */ const hookState = getHookState(currentIndex++, 2); hookState._reducer = reducer; if (!hookState._component) { hookState._value = [ !init ? invokeOrReturn(undefined, initialState) : init(initialState), action => { const currentValue = hookState._nextValue ? hookState._nextValue[0] : hookState._value[0]; const nextValue = hookState._reducer(currentValue, action); if (!ObjectIs(currentValue, nextValue)) { hookState._nextValue = [nextValue, hookState._value[1]]; hookState._component.setState({}); } } ]; hookState._component = currentComponent; if (!currentComponent._hasScuFromHooks) { currentComponent._hasScuFromHooks = true; let prevScu = currentComponent.shouldComponentUpdate; const prevCWU = currentComponent.componentWillUpdate; // If we're dealing with a forced update `shouldComponentUpdate` will // not be called. But we use that to update the hook values, so we // need to call it. currentComponent.componentWillUpdate = function (p, s, c) { if (this._bits & COMPONENT_FORCE) { let tmp = prevScu; // Clear to avoid other sCU hooks from being called prevScu = undefined; updateHookState(p, s, c); prevScu = tmp; } if (prevCWU) prevCWU.call(this, p, s, c); }; // This SCU has the purpose of bailing out after repeated updates // to stateful hooks. // we store the next value in _nextValue[0] and keep doing that for all // state setters, if we have next states and // all next states within a component end up being equal to their original state // we are safe to bail out for this specific component. /** * * @type {import('./internal').Component["shouldComponentUpdate"]} */ // @ts-ignore - We don't use TS to downtranspile // eslint-disable-next-line no-inner-declarations function updateHookState(p, s, c) { if (!hookState._component.__hooks) return true; const hooksList = hookState._component.__hooks._list; // We check whether we have components with a nextValue set that // have values that aren't equal to one another this pushes // us to update further down the tree let shouldUpdate = hookState._component.props !== p || hooksList.every(x => !x._nextValue); hooksList.some(hookItem => { if (hookItem._nextValue) { const currentValue = hookItem._value[0]; hookItem._value = hookItem._nextValue; hookItem._nextValue = undefined; if (!ObjectIs(currentValue, hookItem._value[0])) shouldUpdate = true; } }); return prevScu ? prevScu.call(this, p, s, c) || shouldUpdate : shouldUpdate; } currentComponent.shouldComponentUpdate = updateHookState; } } return hookState._value; } /** * @param {import('./internal').Effect} callback * @param {unknown[]} args * @returns {void} */ export function useEffect(callback, args) { /** @type {import('./internal').EffectHookState} */ const state = getHookState(currentIndex++, 3); if (!options._skipEffects && argsChanged(state._args, args)) { state._value = callback; state._pendingArgs = args; currentComponent.__hooks._pendingEffects.push(state); } } /** * @param {import('./internal').Effect} callback * @param {unknown[]} args * @returns {void} */ export function useLayoutEffect(callback, args) { /** @type {import('./internal').EffectHookState} */ const state = getHookState(currentIndex++, 4); if (!options._skipEffects && argsChanged(state._args, args)) { state._value = callback; state._pendingArgs = args; currentComponent._renderCallbacks.push(state); } } /** @type {(initialValue: unknown) => unknown} */ export function useRef(initialValue) { currentHook = 5; return useMemo(() => ({ current: initialValue }), []); } /** * @param {object} ref * @param {() => object} createHandle * @param {unknown[]} args * @returns {void} */ export function useImperativeHandle(ref, createHandle, args) { currentHook = 6; useLayoutEffect( () => { if (typeof ref == 'function') { const result = ref(createHandle()); return () => { ref(null); if (result && typeof result == 'function') result(); }; } else if (ref) { ref.current = createHandle(); return () => (ref.current = null); } }, args == null ? args : args.concat(ref) ); } /** * @template {unknown} T * @param {() => T} factory * @param {unknown[]} args * @returns {T} */ export function useMemo(factory, args) { /** @type {import('./internal').MemoHookState} */ const state = getHookState(currentIndex++, 7); if (argsChanged(state._args, args)) { state._value = factory(); state._args = args; state._factory = factory; } return state._value; } /** * @param {() => void} callback * @param {unknown[]} args * @returns {() => void} */ export function useCallback(callback, args) { currentHook = 8; return useMemo(() => callback, args); } /** * @param {import('./internal').PreactContext} context */ export function useContext(context) { const provider = currentComponent.context[context._id]; // We could skip this call here, but than we'd not call // `options._hook`. We need to do that in order to make // the devtools aware of this hook. /** @type {import('./internal').ContextHookState} */ const state = getHookState(currentIndex++, 9); // The devtools needs access to the context object to // be able to pull of the default value when no provider // is present in the tree. state._context = context; if (!provider) return context._defaultValue; // This is probably not safe to convert to "!" if (state._value == null) { state._value = true; provider.sub(currentComponent); } return provider.props.value; } /** * Display a custom label for a custom hook for the devtools panel * @type {(value: T, cb?: (value: T) => string | number) => void} */ export function useDebugValue(value, formatter) { if (options.useDebugValue) { options.useDebugValue( formatter ? formatter(value) : /** @type {any}*/ (value) ); } } /** * @param {(error: unknown, errorInfo: import('preact').ErrorInfo) => void} cb * @returns {[unknown, () => void]} */ export function useErrorBoundary(cb) { /** @type {import('./internal').ErrorBoundaryHookState} */ const state = getHookState(currentIndex++, 10); const errState = useState(); state._value = cb; if (!currentComponent.componentDidCatch) { currentComponent.componentDidCatch = (err, errorInfo) => { if (state._value) state._value(err, errorInfo); errState[1](err); }; } return [ errState[0], () => { errState[1](undefined); } ]; } /** @type {() => string} */ export function useId() { /** @type {import('./internal').IdHookState} */ const state = getHookState(currentIndex++, 11); if (!state._value) { // Grab either the root node or the nearest async boundary node. /** @type {import('./internal').VNode} */ let root = currentComponent._vnode; while (root !== null && !root._mask && root._parent !== null) { root = root._parent; } let mask = root._mask || (root._mask = [0, 0]); state._value = 'P' + mask[0] + '-' + mask[1]++; } return state._value; } /** * After paint effects consumer. */ function flushAfterPaintEffects() { let component; while ((component = afterPaintEffects.shift())) { const hooks = component.__hooks; if (!component._parentDom || !hooks) continue; try { hooks._pendingEffects.some(invokeCleanup); hooks._pendingEffects.some(invokeEffect); hooks._pendingEffects = []; } catch (e) { hooks._pendingEffects = []; options._catchError(e, component._vnode); } } } let HAS_RAF = typeof requestAnimationFrame == 'function'; /** * Schedule a callback to be invoked after the browser has a chance to paint a new frame. * Do this by combining requestAnimationFrame (rAF) + setTimeout to invoke a callback after * the next browser frame. * * Also, schedule a timeout in parallel to the the rAF to ensure the callback is invoked * even if RAF doesn't fire (for example if the browser tab is not visible) * * @param {() => void} callback */ function afterNextFrame(callback) { const done = () => { clearTimeout(timeout); if (HAS_RAF) cancelAnimationFrame(raf); setTimeout(callback); }; const timeout = setTimeout(done, RAF_TIMEOUT); let raf; if (HAS_RAF) { raf = requestAnimationFrame(done); } } // Note: if someone used options.debounceRendering = requestAnimationFrame, // then effects will ALWAYS run on the NEXT frame instead of the current one, incurring a ~16ms delay. // Perhaps this is not such a big deal. /** * Schedule afterPaintEffects flush after the browser paints * @param {number} newQueueLength * @returns {void} */ function afterPaint(newQueueLength) { if (newQueueLength === 1 || prevRaf !== options.requestAnimationFrame) { prevRaf = options.requestAnimationFrame; (prevRaf || afterNextFrame)(flushAfterPaintEffects); } } /** * @param {import('./internal').HookState} hook * @returns {void} */ function invokeCleanup(hook) { // A hook cleanup can introduce a call to render which creates a new root, this will call options.vnode // and move the currentComponent away. const comp = currentComponent; let cleanup = hook._cleanup; if (typeof cleanup == 'function') { hook._cleanup = undefined; cleanup(); } currentComponent = comp; } /** * Invoke a Hook's effect * @param {import('./internal').EffectHookState} hook * @returns {void} */ function invokeEffect(hook) { // A hook call can introduce a call to render which creates a new root, this will call options.vnode // and move the currentComponent away. const comp = currentComponent; hook._cleanup = hook._value(); currentComponent = comp; } /** * @param {unknown[]} oldArgs * @param {unknown[]} newArgs * @returns {boolean} */ function argsChanged(oldArgs, newArgs) { return ( !oldArgs || oldArgs.length !== newArgs.length || newArgs.some((arg, index) => !ObjectIs(arg, oldArgs[index])) ); } /** * @template Arg * @param {Arg} arg * @param {(arg: Arg) => any} f * @returns {any} */ function invokeOrReturn(arg, f) { return typeof f == 'function' ? f(arg) : f; } ================================================ FILE: hooks/src/internal.d.ts ================================================ import { Options as PreactOptions, Component as PreactComponent, VNode as PreactVNode, PreactContext, HookType, ErrorInfo } from '../../src/internal'; import { Reducer, StateUpdater } from '.'; export { PreactContext }; export interface Options extends PreactOptions { /** Attach a hook that is invoked before a vnode is diffed. */ _diff?(vnode: VNode): void; /** Attach a hook that is invoked before a vnode has rendered. */ _render?(vnode: VNode): void; /** Attach a hook that is invoked after a vnode has rendered. */ diffed?(vnode: VNode): void; /** Attach a hook that is invoked after a tree was mounted or was updated. */ _commit?(vnode: VNode, commitQueue: Component[]): void; _unmount?(vnode: VNode): void; /** Attach a hook that is invoked before a hook's state is queried. */ _hook?(component: Component, index: number, type: HookType): void; } // Hook tracking export interface ComponentHooks { /** The list of hooks a component uses */ _list: HookState[]; /** List of Effects to be invoked after the next frame is rendered */ _pendingEffects: EffectHookState[]; } export interface Component extends Omit, '_renderCallbacks'> { __hooks?: ComponentHooks; // Extend to include HookStates _renderCallbacks?: Array void)>; _hasScuFromHooks?: boolean; } export interface VNode extends Omit { _mask?: [number, number]; _component?: Component; // Override with our specific Component type } export type HookState = | EffectHookState | MemoHookState | ReducerHookState | ContextHookState | ErrorBoundaryHookState | IdHookState; interface BaseHookState { _value?: unknown; _nextValue?: unknown; _pendingValue?: unknown; _args?: unknown; _pendingArgs?: unknown; _component?: unknown; _cleanup?: unknown; } export type Effect = () => void | Cleanup; export type Cleanup = () => void; export interface EffectHookState extends BaseHookState { _value?: Effect; _args?: unknown[]; _pendingArgs?: unknown[]; _cleanup?: Cleanup | void; } export interface MemoHookState extends BaseHookState { _value?: T; _pendingValue?: T; _args?: unknown[]; _pendingArgs?: unknown[]; _factory?: () => T; } export interface ReducerHookState extends BaseHookState { _nextValue?: [S, StateUpdater]; _value?: [S, StateUpdater]; _component?: Component; _reducer?: Reducer; } export interface ContextHookState extends BaseHookState { /** Whether this hooks as subscribed to updates yet */ _value?: boolean; _context?: PreactContext; } export interface ErrorBoundaryHookState extends BaseHookState { _value?: (error: unknown, errorInfo: ErrorInfo) => void; } export interface IdHookState extends BaseHookState { _value?: string; } ================================================ FILE: hooks/test/_util/useEffectUtil.js ================================================ export function scheduleEffectAssert(assertFn) { return new Promise(resolve => { requestAnimationFrame(() => setTimeout(() => { assertFn(); resolve(); }, 0) ); }); } ================================================ FILE: hooks/test/browser/combinations.test.jsx ================================================ import { setupRerender, act } from 'preact/test-utils'; import { createElement, render, Component, createContext } from 'preact'; import { setupScratch, teardown } from '../../../test/_util/helpers'; import { useState, useReducer, useEffect, useLayoutEffect, useRef, useMemo, useContext } from 'preact/hooks'; import { scheduleEffectAssert } from '../_util/useEffectUtil'; import { vi } from 'vitest'; describe('combinations', () => { /** @type {HTMLDivElement} */ let scratch; /** @type {() => void} */ let rerender; beforeEach(() => { scratch = setupScratch(); rerender = setupRerender(); }); afterEach(() => { teardown(scratch); }); it('can mix useState hooks', () => { const states = {}; const setStates = {}; function Parent() { const [state1, setState1] = useState(1); const [state2, setState2] = useState(2); Object.assign(states, { state1, state2 }); Object.assign(setStates, { setState1, setState2 }); return ; } function Child() { const [state3, setState3] = useState(3); const [state4, setState4] = useState(4); Object.assign(states, { state3, state4 }); Object.assign(setStates, { setState3, setState4 }); return null; } render(, scratch); expect(states).to.deep.equal({ state1: 1, state2: 2, state3: 3, state4: 4 }); setStates.setState2(n => n * 10); setStates.setState3(n => n * 10); rerender(); expect(states).to.deep.equal({ state1: 1, state2: 20, state3: 30, state4: 4 }); }); it('can rerender asynchronously from within an effect', () => { const didRender = vi.fn(); function Comp() { const [counter, setCounter] = useState(0); useEffect(() => { if (counter === 0) setCounter(1); }); didRender(counter); return null; } render(, scratch); return scheduleEffectAssert(() => { rerender(); expect(didRender).toHaveBeenCalledTimes(2); expect(didRender).toHaveBeenCalledWith(1); }); }); it('can rerender synchronously from within a layout effect', () => { const didRender = vi.fn(); function Comp() { const [counter, setCounter] = useState(0); useLayoutEffect(() => { if (counter === 0) setCounter(1); }); didRender(counter); return null; } render(, scratch); rerender(); expect(didRender).toHaveBeenCalledTimes(2); expect(didRender).toHaveBeenCalledWith(1); }); it('can access refs from within a layout effect callback', () => { let refAtLayoutTime; function Comp() { const input = useRef(); useLayoutEffect(() => { refAtLayoutTime = input.current; }); return ; } render(, scratch); expect(refAtLayoutTime.value).to.equal('hello'); }); it('can use multiple useState and useReducer hooks', () => { let states = []; let dispatchState4; function reducer1(state, action) { switch (action.type) { case 'increment': return state + action.count; } } function reducer2(state, action) { switch (action.type) { case 'increment': return state + action.count * 2; } } function Comp() { const [state1] = useState(0); const [state2] = useReducer(reducer1, 10); const [state3] = useState(1); const [state4, dispatch] = useReducer(reducer2, 20); dispatchState4 = dispatch; states.push(state1, state2, state3, state4); return null; } render(, scratch); expect(states).to.deep.equal([0, 10, 1, 20]); states = []; dispatchState4({ type: 'increment', count: 10 }); rerender(); expect(states).to.deep.equal([0, 10, 1, 40]); }); it('ensures useEffect always schedule after the next paint following a redraw effect, when using the default debounce strategy', () => { let effectCount = 0; function Comp() { const [counter, setCounter] = useState(0); useEffect(() => { if (counter === 0) setCounter(1); effectCount++; }); return null; } render(, scratch); return scheduleEffectAssert(() => { expect(effectCount).to.equal(1); }); }); it('should not reuse functional components with hooks', () => { let updater = { first: undefined, second: undefined }; function Foo(props) { let [v, setter] = useState(0); updater[props.id] = () => setter(++v); return
    {v}
    ; } let updateParent; class App extends Component { constructor(props) { super(props); this.state = { active: true }; updateParent = () => this.setState(p => ({ active: !p.active })); } render() { return (
    {this.state.active && }
    ); } } render(, scratch); act(() => updater.second()); expect(scratch.textContent).to.equal('01'); updateParent(); rerender(); expect(scratch.textContent).to.equal('1'); updateParent(); rerender(); expect(scratch.textContent).to.equal('01'); }); it('should have a right call order with correct dom ref', () => { let i = 0, set; const calls = []; function Inner() { useLayoutEffect(() => { calls.push('layout inner call ' + scratch.innerHTML); return () => calls.push('layout inner dispose ' + scratch.innerHTML); }); useEffect(() => { calls.push('effect inner call ' + scratch.innerHTML); return () => calls.push('effect inner dispose ' + scratch.innerHTML); }); return hello {i}; } function Outer() { i++; const [state, setState] = useState(false); set = () => setState(!state); useLayoutEffect(() => { calls.push('layout outer call ' + scratch.innerHTML); return () => calls.push('layout outer dispose ' + scratch.innerHTML); }); useEffect(() => { calls.push('effect outer call ' + scratch.innerHTML); return () => calls.push('effect outer dispose ' + scratch.innerHTML); }); return ; } act(() => render(, scratch)); expect(calls).to.deep.equal([ 'layout inner call hello 1', 'layout outer call hello 1', 'effect inner call hello 1', 'effect outer call hello 1' ]); // NOTE: this order is (at the time of writing) intentionally different from // React. React calls all disposes across all components, and then invokes all // effects across all components. We call disposes and effects in order of components: // for each component, call its disposes and then its effects. If presented with a // compelling use case to support inter-component dispose dependencies, then rewrite this // test to test React's order. In other words, if there is a use case to support calling // all disposes across components then re-order the lines below to demonstrate the desired behavior. act(() => set()); expect(calls).to.deep.equal([ 'layout inner call hello 1', 'layout outer call hello 1', 'effect inner call hello 1', 'effect outer call hello 1', 'layout inner dispose hello 2', 'layout inner call hello 2', 'layout outer dispose hello 2', 'layout outer call hello 2', 'effect inner dispose hello 2', 'effect inner call hello 2', 'effect outer dispose hello 2', 'effect outer call hello 2' ]); }); // TODO: I actually think this is an acceptable failure, because we update child first and then parent // the effects are out of order it.skip('should run effects child-first even for children separated by memoization', () => { let ops = []; /** @type {() => void} */ let updateChild; /** @type {() => void} */ let updateParent; function Child() { const [, setCount] = useState(0); updateChild = () => setCount(c => c + 1); useEffect(() => { ops.push('child effect'); }); return
    Child
    ; } function Parent() { const [, setCount] = useState(0); updateParent = () => setCount(c => c + 1); const memoedChild = useMemo(() => , []); useEffect(() => { ops.push('parent effect'); }); return (
    Parent
    {memoedChild}
    ); } act(() => render(, scratch)); expect(ops).to.deep.equal(['child effect', 'parent effect']); ops = []; updateChild(); updateParent(); act(() => rerender()); expect(ops).to.deep.equal(['child effect', 'parent effect']); }); it('should not block hook updates when context updates are enqueued', () => { const Ctx = createContext({ value: 0, setValue: /** @type {*} */ () => {} }); let triggerSubmit = () => {}; function Child() { const ctx = useContext(Ctx); const [shouldSubmit, setShouldSubmit] = useState(false); triggerSubmit = () => setShouldSubmit(true); useEffect(() => { if (shouldSubmit) { // Update parent state and child state at the same time ctx.setValue(v => v + 1); setShouldSubmit(false); } }, [shouldSubmit]); return

    {ctx.value}

    ; } function App() { const [value, setValue] = useState(0); const ctx = useMemo(() => { return { value, setValue }; }, [value]); return ( ); } act(() => { render(, scratch); }); expect(scratch.textContent).to.equal('0'); act(() => { triggerSubmit(); }); expect(scratch.textContent).to.equal('1'); // This is where the update wasn't applied act(() => { triggerSubmit(); }); expect(scratch.textContent).to.equal('2'); }); it('parent and child refs should be set before all effects', () => { const anchorId = 'anchor'; const tooltipId = 'tooltip'; const effectLog = []; let useRef2 = vi.fn(init => { const realRef = useRef(init); const ref = useRef(init); Object.defineProperty(ref, 'current', { get: () => realRef.current, set: value => { realRef.current = value; effectLog.push('set ref ' + value?.tagName); } }); return ref; }); function Tooltip({ anchorRef, children }) { // For example, used to manually position the tooltip const tooltipRef = useRef2(null); useLayoutEffect(() => { expect(anchorRef.current?.id).to.equal(anchorId); expect(tooltipRef.current?.id).to.equal(tooltipId); effectLog.push('tooltip layout effect'); }, [anchorRef, tooltipRef]); useEffect(() => { expect(anchorRef.current?.id).to.equal(anchorId); expect(tooltipRef.current?.id).to.equal(tooltipId); effectLog.push('tooltip effect'); }, [anchorRef, tooltipRef]); return (
    {children}
    ); } function App() { // For example, used to define what element to anchor the tooltip to const anchorRef = useRef2(null); useLayoutEffect(() => { expect(anchorRef.current?.id).to.equal(anchorId); effectLog.push('anchor layout effect'); }, [anchorRef]); useEffect(() => { expect(anchorRef.current?.id).to.equal(anchorId); effectLog.push('anchor effect'); }, [anchorRef]); return (

    More info

    a tooltip
    ); } act(() => { render(, scratch); }); expect(effectLog).to.deep.equal([ 'set ref P', 'set ref DIV', 'tooltip layout effect', 'anchor layout effect', 'tooltip effect', 'anchor effect' ]); }); it('should not loop infinitely', () => { const actions = []; let toggle; function App() { const [value, setValue] = useState(false); const data = useMemo(() => { actions.push('memo'); return {}; }, [value]); const [prevData, setPreviousData] = useState(data); if (prevData !== data) { setPreviousData(data); } actions.push('render'); toggle = () => setValue(!value); return
    Value: {JSON.stringify(value)}
    ; } act(() => { render(, scratch); }); expect(actions).to.deep.equal(['memo', 'render']); expect(scratch.innerHTML).to.deep.equal('
    Value: false
    '); act(() => { toggle(); }); expect(actions).to.deep.equal([ 'memo', 'render', 'memo', 'render', 'render' ]); expect(scratch.innerHTML).to.deep.equal('
    Value: true
    '); }); }); ================================================ FILE: hooks/test/browser/componentDidCatch.test.jsx ================================================ import { createElement, render, Component } from 'preact'; import { act } from 'preact/test-utils'; import { setupScratch, teardown } from '../../../test/_util/helpers'; import { useEffect } from 'preact/hooks'; describe('errorInfo', () => { /** @type {HTMLDivElement} */ let scratch; beforeEach(() => { scratch = setupScratch(); }); afterEach(() => { teardown(scratch); }); it('should pass errorInfo on hook unmount error', () => { let info; let update; class Receiver extends Component { constructor(props) { super(props); this.state = { error: null, i: 0 }; update = this.setState.bind(this); } componentDidCatch(error, errorInfo) { info = errorInfo; this.setState({ error }); } render() { if (this.state.error) return
    ; if (this.state.i === 0) return ; return null; } } function ThrowErr() { useEffect(() => { return () => { throw new Error('fail'); }; }, []); return

    ; } act(() => { render(, scratch); }); act(() => { update({ i: 1 }); }); expect(info).to.deep.equal({}); }); }); ================================================ FILE: hooks/test/browser/errorBoundary.test.jsx ================================================ import { Fragment, createElement, render } from 'preact'; import { setupScratch, teardown } from '../../../test/_util/helpers'; import { useErrorBoundary, useLayoutEffect, useState } from 'preact/hooks'; import { setupRerender } from 'preact/test-utils'; import { vi } from 'vitest'; describe('errorBoundary', () => { /** @type {HTMLDivElement} */ let scratch, rerender; beforeEach(() => { scratch = setupScratch(); rerender = setupRerender(); }); afterEach(() => { teardown(scratch); }); it('catches errors', () => { let resetErr, success = false; const Throws = () => { throw new Error('test'); }; const App = props => { const [err, reset] = useErrorBoundary(); resetErr = reset; return err ?

    Error

    : success ?

    Success

    : ; }; render(, scratch); rerender(); expect(scratch.innerHTML).to.equal('

    Error

    '); success = true; resetErr(); rerender(); expect(scratch.innerHTML).to.equal('

    Success

    '); }); it('calls the errorBoundary callback', () => { const spy = vi.fn(); const error = new Error('test'); const Throws = () => { throw error; }; const App = props => { const [err] = useErrorBoundary(spy); return err ?

    Error

    : ; }; render(, scratch); rerender(); expect(scratch.innerHTML).to.equal('

    Error

    '); expect(spy).toHaveBeenCalledOnce(); expect(spy).toHaveBeenCalledWith(error, {}); }); it('returns error', () => { const error = new Error('test'); const Throws = () => { throw error; }; let returned; const App = () => { const [err] = useErrorBoundary(); returned = err; return err ?

    Error

    : ; }; render(, scratch); rerender(); expect(returned).to.equal(error); }); it('does not leave a stale closure', () => { const spy = vi.fn(), spy2 = vi.fn(); let resetErr; const error = new Error('test'); const Throws = () => { throw error; }; const App = props => { const [err, reset] = useErrorBoundary(props.onError); resetErr = reset; return err ?

    Error

    : ; }; render(, scratch); rerender(); expect(scratch.innerHTML).to.equal('

    Error

    '); expect(spy).toHaveBeenCalledOnce(); expect(spy).toHaveBeenCalledWith(error, {}); resetErr(); render(, scratch); rerender(); expect(spy).toHaveBeenCalledOnce(); expect(spy2).toHaveBeenCalledOnce(); expect(spy2).toHaveBeenCalledWith(error, {}); expect(scratch.innerHTML).to.equal('

    Error

    '); }); it('does not invoke old effects when a cleanup callback throws an error and is handled', () => { let throwErr = false; let thrower = vi.fn(() => { if (throwErr) { throw new Error('test'); } }); let badEffect = vi.fn(() => thrower); let goodEffect = vi.fn(); function EffectThrowsError() { useLayoutEffect(badEffect); return Test; } function Child({ children }) { useLayoutEffect(goodEffect); return children; } function App() { const [err] = useErrorBoundary(); return err ?

    Error

    : ; } render(, scratch); expect(scratch.innerHTML).to.equal('Test'); expect(badEffect).toHaveBeenCalledOnce(); expect(goodEffect).toHaveBeenCalledOnce(); throwErr = true; render(, scratch); rerender(); expect(scratch.innerHTML).to.equal('

    Error

    '); expect(thrower).toHaveBeenCalledOnce(); expect(badEffect).toHaveBeenCalledOnce(); expect(goodEffect).toHaveBeenCalledOnce(); }); it('should not duplicate in lists where an item throws and the parent catches and returns a differing type', () => { const baseTodos = [ { text: 'first item', completed: false }, { text: 'Test the feature', completed: false }, { text: 'another item', completed: false } ]; function TodoList() { const [todos, setTodos] = useState([...baseTodos]); return (
      {todos.map((todo, index) => ( { todos[index] = { ...todos[index], completed: !todos[index].completed }; setTodos([...todos]); }} todo={todo} index={index} /> ))}
    ); } function TodoItem(props) { const [error] = useErrorBoundary(); if (error) { return
  • An error occurred: {error}
  • ; } return ; } let set; function TodoItemInner({ todo, index, toggleTodo }) { if (todo.completed) { throw new Error('Todo completed!'); } if (index === 1) { set = toggleTodo; } return (
  • ); } render(, scratch); expect(scratch.innerHTML).to.equal( '
    ' ); set(); rerender(); expect(scratch.innerHTML).to.equal( '
    • An error occurred:
    ' ); }); }); ================================================ FILE: hooks/test/browser/hooks.options.test.jsx ================================================ import { afterDiffSpy, beforeRenderSpy, unmountSpy, hookSpy } from '../../../test/_util/optionSpies'; import { setupRerender, act } from 'preact/test-utils'; import { createElement, render, createContext, options } from 'preact'; import { setupScratch, teardown } from '../../../test/_util/helpers'; import { useState, useReducer, useEffect, useLayoutEffect, useRef, useImperativeHandle, useMemo, useCallback, useContext, useErrorBoundary } from 'preact/hooks'; import { vi } from 'vitest'; describe('hook options', () => { /** @type {HTMLDivElement} */ let scratch; /** @type {() => void} */ let rerender; /** @type {() => void} */ let increment; beforeEach(() => { scratch = setupScratch(); rerender = setupRerender(); afterDiffSpy.mockClear(); unmountSpy.mockClear(); beforeRenderSpy.mockClear(); hookSpy.mockClear(); }); afterEach(() => { teardown(scratch); }); function App() { const [count, setCount] = useState(0); increment = () => setCount(prevCount => prevCount + 1); return
    {count}
    ; } it('should call old options on mount', () => { render(, scratch); expect(beforeRenderSpy).toHaveBeenCalled(); expect(afterDiffSpy).toHaveBeenCalled(); }); it('should call old options.diffed on update', () => { render(, scratch); increment(); rerender(); expect(beforeRenderSpy).toHaveBeenCalled(); expect(afterDiffSpy).toHaveBeenCalled(); }); it('should call old options on unmount', () => { render(, scratch); render(null, scratch); expect(unmountSpy).toHaveBeenCalled(); }); it('should detect hooks', () => { const USE_STATE = 1; const USE_REDUCER = 2; const USE_EFFECT = 3; const USE_LAYOUT_EFFECT = 4; const USE_REF = 5; const USE_IMPERATIVE_HANDLE = 6; const USE_MEMO = 7; const USE_CALLBACK = 8; const USE_CONTEXT = 9; const USE_ERROR_BOUNDARY = 10; const Ctx = createContext(null); function App() { useState(0); useReducer(x => x, 0); useEffect(() => null, []); useLayoutEffect(() => null, []); const ref = useRef(null); useImperativeHandle(ref, () => null); useMemo(() => null, []); useCallback(() => null, []); useContext(Ctx); useErrorBoundary(() => null); } render( , scratch ); expect(hookSpy.mock.calls.map(arg => [arg[1], arg[2]])).to.deep.equal([ [0, USE_STATE], [1, USE_REDUCER], [2, USE_EFFECT], [3, USE_LAYOUT_EFFECT], [4, USE_REF], [5, USE_IMPERATIVE_HANDLE], [6, USE_MEMO], [7, USE_CALLBACK], [8, USE_CONTEXT], [9, USE_ERROR_BOUNDARY], // Belongs to useErrorBoundary that uses multiple native hooks. [10, USE_STATE] ]); }); describe('Effects', () => { beforeEach(() => { options._skipEffects = options.__s = true; }); afterEach(() => { options._skipEffects = options.__s = false; }); it('should skip effect hooks', () => { const spy = vi.fn(); function App() { useEffect(spy, []); useLayoutEffect(spy, []); return null; } act(() => { render(, scratch); }); expect(spy).not.toHaveBeenCalled(); }); }); }); ================================================ FILE: hooks/test/browser/useCallback.test.jsx ================================================ import { createElement, render } from 'preact'; import { setupScratch, teardown } from '../../../test/_util/helpers'; import { useCallback } from 'preact/hooks'; describe('useCallback', () => { /** @type {HTMLDivElement} */ let scratch; beforeEach(() => { scratch = setupScratch(); }); afterEach(() => { teardown(scratch); }); it('only recomputes the callback when inputs change', () => { const callbacks = []; function Comp({ a, b }) { const cb = useCallback(() => a + b, [a, b]); callbacks.push(cb); return null; } render(, scratch); render(, scratch); expect(callbacks[0]).to.equal(callbacks[1]); expect(callbacks[0]()).to.equal(2); render(, scratch); render(, scratch); expect(callbacks[1]).to.not.equal(callbacks[2]); expect(callbacks[2]).to.equal(callbacks[3]); expect(callbacks[2]()).to.equal(3); }); }); ================================================ FILE: hooks/test/browser/useContext.test.jsx ================================================ import { createElement, render, createContext, Component } from 'preact'; import { act } from 'preact/test-utils'; import { setupScratch, teardown } from '../../../test/_util/helpers'; import { useContext, useEffect, useState } from 'preact/hooks'; import { vi } from 'vitest'; describe('useContext', () => { /** @type {HTMLDivElement} */ let scratch; beforeEach(() => { scratch = setupScratch(); }); afterEach(() => { teardown(scratch); }); it('gets values from context', () => { const values = []; const Context = createContext(13); function Comp() { const value = useContext(Context); values.push(value); return null; } render(, scratch); render( , scratch ); render( , scratch ); expect(values).to.deep.equal([13, 42, 69]); }); it('should use default value', () => { const Foo = createContext(42); const spy = vi.fn(); function App() { spy(useContext(Foo)); return
    ; } render(, scratch); expect(spy).toHaveBeenCalledWith(42); }); it('should update when value changes with nonUpdating Component on top', async () => { const spy = vi.fn(); const Ctx = createContext(0); class NoUpdate extends Component { shouldComponentUpdate() { return false; } render() { return this.props.children; } } function App(props) { return ( ); } function Comp() { const value = useContext(Ctx); spy(value); return

    {value}

    ; } render(, scratch); expect(spy).toHaveBeenCalledOnce(); expect(spy).toHaveBeenCalledWith(0); render(, scratch); return new Promise(resolve => { // Wait for enqueued hook update setTimeout(() => { // Should not be called a third time expect(spy).toHaveBeenCalledTimes(2); expect(spy).toHaveBeenCalledWith(1); resolve(); }, 0); }); }); it('should only update when value has changed', async () => { const spy = vi.fn(); const Ctx = createContext(0); function App(props) { return ( ); } function Comp() { const value = useContext(Ctx); spy(value); return

    {value}

    ; } render(, scratch); expect(spy).toHaveBeenCalledOnce(); expect(spy).toHaveBeenCalledWith(0); render(, scratch); expect(spy).toHaveBeenCalledTimes(2); expect(spy).toHaveBeenCalledWith(1); return new Promise(resolve => { // Wait for enqueued hook update setTimeout(() => { // Should not be called a third time expect(spy).toHaveBeenCalledTimes(2); resolve(); }, 0); }); }); it('should allow multiple context hooks at the same time', () => { const Foo = createContext(0); const Bar = createContext(10); const spy = vi.fn(); const unmountspy = vi.fn(); function Comp() { const foo = useContext(Foo); const bar = useContext(Bar); spy(foo, bar); useEffect(() => () => unmountspy()); return
    ; } render( , scratch ); expect(spy).toHaveBeenCalledOnce(); expect(spy).toHaveBeenCalledWith(0, 10); render( , scratch ); expect(spy).toHaveBeenCalledTimes(2); expect(unmountspy).not.toHaveBeenCalled(); }); it('should only subscribe a component once', () => { const values = []; const Context = createContext(13); let provider, subSpy; function Comp() { provider = this._vnode._parent._component; const value = useContext(Context); values.push(value); return null; } render(, scratch); render( , scratch ); subSpy = vi.spyOn(provider, 'sub'); render( , scratch ); expect(subSpy).not.toHaveBeenCalled(); expect(values).to.deep.equal([13, 42, 69]); }); it('should only subscribe a component once (non-provider)', () => { const values = []; const Context = createContext(13); let provider, subSpy; function Comp() { provider = this._vnode._parent._component; const value = useContext(Context); values.push(value); return null; } render(, scratch); render( , scratch ); subSpy = vi.spyOn(provider, 'sub'); render( , scratch ); expect(subSpy).not.toHaveBeenCalled(); expect(values).to.deep.equal([13, 42, 69]); }); it('should maintain context', async () => { const context = createContext(null); const { Provider } = context; const first = { name: 'first' }; const second = { name: 'second' }; const Input = () => { const config = useContext(context); // Avoid eslint complaining about unused first value const state = useState('initial'); const set = state[1]; useEffect(() => { // Schedule the update on the next frame requestAnimationFrame(() => { set('irrelevant'); }); }, [config]); return
    {config.name}
    ; }; const App = props => { const [config, setConfig] = useState({}); useEffect(() => { setConfig(props.config); }, [props.config]); return ( ); }; act(() => { render(, scratch); // Create a new div to append the `second` case const div = scratch.appendChild(document.createElement('div')); render(, div); }); return new Promise(resolve => { // Push the expect into the next frame requestAnimationFrame(() => { expect(scratch.innerHTML).equal( '
    first
    second
    ' ); resolve(); }); }); }); it('should not rerender consumers that have been unmounted', () => { const context = createContext(0); const Provider = context.Provider; const Inner = vi.fn(() => { const value = useContext(context); return
    {value}
    ; }); let toggleConsumer; let changeValue; class App extends Component { constructor() { super(); this.state = { value: 0, show: true }; changeValue = value => this.setState({ value }); toggleConsumer = () => this.setState(({ show }) => ({ show: !show })); } render(props, state) { return (
    {state.show ? : null}
    ); } } render(, scratch); expect(scratch.innerHTML).to.equal('
    0
    '); expect(Inner).toHaveBeenCalledOnce(); act(() => changeValue(1)); expect(scratch.innerHTML).to.equal('
    1
    '); expect(Inner).toHaveBeenCalledTimes(2); act(() => toggleConsumer()); expect(scratch.innerHTML).to.equal('
    '); expect(Inner).toHaveBeenCalledTimes(2); act(() => changeValue(2)); expect(scratch.innerHTML).to.equal('
    '); expect(Inner).toHaveBeenCalledTimes(2); }); it('should rerender when reset to defaultValue', () => { const defaultValue = { state: 'hi' }; const context = createContext(defaultValue); let set; const Consumer = () => { const ctx = useContext(context); return

    {ctx.state}

    ; }; class NoUpdate extends Component { shouldComponentUpdate() { return false; } render() { return ; } } const Provider = () => { const [state, setState] = useState(defaultValue); set = setState; return ( ); }; render(, scratch); expect(scratch.innerHTML).to.equal('

    hi

    '); act(() => { set({ state: 'bye' }); }); expect(scratch.innerHTML).to.equal('

    bye

    '); act(() => { set(defaultValue); }); expect(scratch.innerHTML).to.equal('

    hi

    '); }); }); ================================================ FILE: hooks/test/browser/useDebugValue.test.jsx ================================================ import { createElement, render, options } from 'preact'; import { setupScratch, teardown } from '../../../test/_util/helpers'; import { useDebugValue, useState } from 'preact/hooks'; import { vi } from 'vitest'; describe('useDebugValue', () => { /** @type {HTMLDivElement} */ let scratch; beforeEach(() => { scratch = setupScratch(); }); afterEach(() => { teardown(scratch); delete options.useDebugValue; }); it('should do nothing when no options hook is present', () => { function useFoo() { useDebugValue('foo'); return useState(0); } function App() { let [v] = useFoo(); return
    {v}
    ; } expect(() => render(, scratch)).to.not.throw(); }); it('should call options hook with value', () => { let spy = (options.useDebugValue = vi.fn()); function useFoo() { useDebugValue('foo'); return useState(0); } function App() { let [v] = useFoo(); return
    {v}
    ; } render(, scratch); expect(spy).toHaveBeenCalledOnce(); expect(spy).toHaveBeenCalledWith('foo'); }); it('should apply optional formatter', () => { let spy = (options.useDebugValue = vi.fn()); function useFoo() { useDebugValue('foo', x => x + 'bar'); return useState(0); } function App() { let [v] = useFoo(); return
    {v}
    ; } render(, scratch); expect(spy).toHaveBeenCalledOnce(); expect(spy).toHaveBeenCalledWith('foobar'); }); }); ================================================ FILE: hooks/test/browser/useEffect.test.jsx ================================================ import { Component, Fragment, createElement, render } from 'preact'; import { useEffect, useRef, useState } from 'preact/hooks'; import { act, teardown as teardownAct } from 'preact/test-utils'; import { vi } from 'vitest'; import { setupScratch, teardown } from '../../../test/_util/helpers'; import { scheduleEffectAssert } from '../_util/useEffectUtil'; import { useEffectAssertions } from './useEffectAssertions'; describe('useEffect', () => { /** @type {HTMLDivElement} */ let scratch; beforeEach(() => { scratch = setupScratch(); }); afterEach(() => { teardown(scratch); }); useEffectAssertions(useEffect, scheduleEffectAssert); it('calls the effect immediately if another render is about to start', () => { const cleanupFunction = vi.fn(); const callback = vi.fn(() => cleanupFunction); function Comp() { useEffect(callback); return null; } render(, scratch); render(, scratch); expect(cleanupFunction).not.toHaveBeenCalled(); expect(callback).toHaveBeenCalledOnce(); render(, scratch); expect(cleanupFunction).toHaveBeenCalledOnce(); expect(callback).toHaveBeenCalledTimes(2); }); it('cancels the effect when the component get unmounted before it had the chance to run it', () => { const cleanupFunction = vi.fn(); const callback = vi.fn(() => cleanupFunction); function Comp() { useEffect(callback); return null; } render(, scratch); render(null, scratch); return scheduleEffectAssert(() => { expect(cleanupFunction).not.toHaveBeenCalled(); expect(callback).not.toHaveBeenCalled(); }); }); it('should execute multiple effects in same component in the right order', () => { let executionOrder = []; const App = ({ i }) => { executionOrder = []; useEffect(() => { executionOrder.push('action1'); return () => executionOrder.push('cleanup1'); }, [i]); useEffect(() => { executionOrder.push('action2'); return () => executionOrder.push('cleanup2'); }, [i]); return

    Test

    ; }; act(() => render(, scratch)); act(() => render(, scratch)); expect(executionOrder).to.deep.equal([ 'cleanup1', 'cleanup2', 'action1', 'action2' ]); }); it('should execute effects in parent if child throws in effect', async () => { const executionOrder = []; const Child = () => { useEffect(() => { executionOrder.push('child'); throw new Error('test'); }, []); useEffect(() => { executionOrder.push('child after throw'); return () => executionOrder.push('child after throw cleanup'); }, []); return

    Test

    ; }; const Parent = () => { useEffect(() => { executionOrder.push('parent'); return () => executionOrder.push('parent cleanup'); }, []); return ; }; class ErrorBoundary extends Component { componentDidCatch(error) { this.setState({ error }); } render({ children }, { error }) { return error ?
    error
    : children; } } act(() => render( , scratch ) ); expect(executionOrder).to.deep.equal(['child', 'parent', 'parent cleanup']); expect(scratch.innerHTML).to.equal('
    error
    '); }); it('should throw an error upwards', () => { const spy = vi.fn(); let errored = false; const Page1 = () => { const [state, setState] = useState('loading'); useEffect(() => { setState('loaded'); }, []); return

    {state}

    ; }; const Page2 = () => { useEffect(() => { throw new Error('err'); }, []); return

    invisible

    ; }; class App extends Component { componentDidCatch(err) { spy(); errored = err; this.forceUpdate(); } render(props, state) { if (errored) { return

    Error

    ; } return {props.page === 1 ? : }; } } act(() => render(, scratch)); expect(spy).not.toHaveBeenCalled(); expect(scratch.innerHTML).to.equal('

    loaded

    '); act(() => render(, scratch)); expect(spy).toHaveBeenCalledOnce(); expect(scratch.innerHTML).to.equal('

    Error

    '); errored = false; act(() => render(, scratch)); expect(spy).toHaveBeenCalledOnce(); expect(scratch.innerHTML).to.equal('

    loaded

    '); }); it('should throw an error upwards from return', () => { const spy = vi.fn(); let errored = false; const Page1 = () => { const [state, setState] = useState('loading'); useEffect(() => { setState('loaded'); }, []); return

    {state}

    ; }; const Page2 = () => { useEffect(() => { return () => { throw new Error('err'); }; }, []); return

    Load

    ; }; class App extends Component { componentDidCatch(err) { spy(); errored = err; this.forceUpdate(); } render(props, state) { if (errored) { return

    Error

    ; } return {props.page === 1 ? : }; } } act(() => render(, scratch)); expect(scratch.innerHTML).to.equal('

    Load

    '); act(() => render(, scratch)); expect(spy).toHaveBeenCalledOnce(); expect(scratch.innerHTML).to.equal('

    Error

    '); }); it('catches errors when error is invoked during render', () => { const spy = vi.fn(); let errored; function Comp() { useEffect(() => { throw new Error('hi'); }); return null; } class App extends Component { componentDidCatch(err) { spy(); errored = err; this.forceUpdate(); } render(props, state) { if (errored) { return

    Error

    ; } return ; } } render(, scratch); act(() => { render(, scratch); }); expect(spy).toHaveBeenCalledOnce(); expect(errored).to.be.an('Error').with.property('message', 'hi'); expect(scratch.innerHTML).to.equal('

    Error

    '); }); it('should allow creating a new root', () => { const root = document.createElement('div'); const global = document.createElement('div'); scratch.appendChild(root); scratch.appendChild(global); const Modal = props => { const [, setCanProceed] = useState(true); const ChildProp = props.content; return (
    ); }; const Inner = () => { useEffect(() => { render(
    global
    , global); }, []); return
    Inner
    ; }; act(() => { render( { props.setCanProceed(false); return ; }} />, root ); }); expect(scratch.innerHTML).to.equal( '
    Inner
    global
    ' ); }); it('should not crash when effect returns truthy non-function value', () => { const callback = vi.fn(() => 'truthy'); function Comp() { useEffect(callback); return null; } render(, scratch); render(, scratch); expect(callback).toHaveBeenCalledOnce(); render(
    Replacement
    , scratch); }); it('support render roots from an effect', async () => { let promise, increment; const Counter = () => { const [count, setCount] = useState(0); const renderRoot = useRef(); useEffect(() => { if (count > 0) { const div = renderRoot.current; return () => render(, div); } return () => 'test'; }, [count]); increment = () => { setCount(x => x + 1); promise = new Promise(res => { setTimeout(() => { setCount(x => x + 1); res(); }); }); }; return (
    Count: {count}
    ); }; const Dummy = () =>
    dummy
    ; render(, scratch); expect(scratch.innerHTML).to.equal( '
    Count: 0
    ' ); act(() => { increment(); }); await promise; act(() => {}); expect(scratch.innerHTML).to.equal( '
    Count: 2
    dummy
    ' ); }); it('hooks should be called in right order', async () => { teardownAct(); let increment; const Counter = () => { const [count, setCount] = useState(0); useState('binggo!!'); const renderRoot = useRef(); useEffect(() => { const div = renderRoot.current; render(, div); }, [count]); increment = () => { setCount(x => x + 1); return Promise.resolve().then(() => setCount(x => x + 1)); }; return (
    Count: {count}
    ); }; const Dummy = () => { useState(); return
    dummy
    ; }; render(, scratch); expect(scratch.innerHTML).to.equal( '
    Count: 0
    ' ); /** Using the act function will affect the timing of the useEffect */ await increment(); expect(scratch.innerHTML).to.equal( '
    Count: 2
    dummy
    ' ); }); it('handles errors correctly', () => { class ErrorBoundary extends Component { constructor(props) { super(props); this.state = { error: null }; } componentDidCatch(error) { this.setState({ error: 'oh no' }); } render() { return this.state.error ? (

    Error! {this.state.error}

    ) : ( this.props.children ); } } let update; const firstEffectSpy = vi.fn(); const firstEffectcleanup = vi.fn(); const secondEffectSpy = vi.fn(); const secondEffectcleanup = vi.fn(); const MainContent = () => { const [val, setVal] = useState(false); update = () => setVal(!val); useEffect(() => { firstEffectSpy(); return () => { firstEffectcleanup(); throw new Error('oops'); }; }, [val]); useEffect(() => { secondEffectSpy(); return () => { secondEffectcleanup(); }; }, []); return

    Hello world

    ; }; act(() => { render( , scratch ); }); expect(firstEffectSpy).toHaveBeenCalledOnce(); expect(secondEffectSpy).toHaveBeenCalledOnce(); act(() => { update(); }); expect(firstEffectSpy).toHaveBeenCalledOnce(); expect(secondEffectSpy).toHaveBeenCalledOnce(); expect(firstEffectcleanup).toHaveBeenCalledOnce(); expect(secondEffectcleanup).toHaveBeenCalledOnce(); }); it('orders effects effectively', () => { const calls = []; const GrandChild = ({ id }) => { useEffect(() => { calls.push(`${id} - Effect`); return () => { calls.push(`${id} - Cleanup`); }; }, [id]); return

    {id}

    ; }; const Child = ({ id }) => { useEffect(() => { calls.push(`${id} - Effect`); return () => { calls.push(`${id} - Cleanup`); }; }, [id]); return ( ); }; function Parent() { useEffect(() => { calls.push('Parent - Effect'); return () => { calls.push('Parent - Cleanup'); }; }, []); return (
    ); } act(() => { render(, scratch); }); expect(calls).to.deep.equal([ 'Child-1-GrandChild-1 - Effect', 'Child-1-GrandChild-2 - Effect', 'Child-1 - Effect', 'Child-2-GrandChild-1 - Effect', 'Child-2-GrandChild-2 - Effect', 'Child-2 - Effect', 'Child-3-GrandChild-1 - Effect', 'Child-3-GrandChild-2 - Effect', 'Child-3 - Effect', 'Parent - Effect' ]); }); it('should cancel effects from a disposed render', () => { const calls = []; const App = () => { const [greeting, setGreeting] = useState('bye'); useEffect(() => { calls.push('doing effect' + greeting); return () => { calls.push('cleaning up' + greeting); }; }, [greeting]); if (greeting === 'bye') { setGreeting('hi'); } return

    {greeting}

    ; }; act(() => { render(, scratch); }); expect(calls.length).to.equal(1); expect(calls).to.deep.equal(['doing effecthi']); }); it('should not rerun committed effects', () => { const calls = []; const App = ({ i }) => { const [greeting, setGreeting] = useState('hi'); useEffect(() => { calls.push('doing effect' + greeting); return () => { calls.push('cleaning up' + greeting); }; }, []); if (i === 2) { setGreeting('bye'); } return

    {greeting}

    ; }; act(() => { render(, scratch); }); expect(calls.length).to.equal(1); expect(calls).to.deep.equal(['doing effecthi']); act(() => { render(, scratch); }); }); it('should not schedule effects that have no change', () => { const calls = []; let set; const App = ({ i }) => { const [greeting, setGreeting] = useState('hi'); set = setGreeting; useEffect(() => { calls.push('doing effect' + greeting); return () => { calls.push('cleaning up' + greeting); }; }, [greeting]); if (greeting === 'bye') { setGreeting('hi'); } return

    {greeting}

    ; }; act(() => { render(, scratch); }); expect(calls.length).to.equal(1); expect(calls).to.deep.equal(['doing effecthi']); act(() => { set('bye'); }); expect(calls.length).to.equal(1); expect(calls).to.deep.equal(['doing effecthi']); }); it('should not crash when effect throws and component is unmounted by render(null) during flush', () => { // In flushAfterPaintEffects(): // 1. Guard checks component.__hooks — truthy, passes // 2. invokeEffect runs the effect callback // 3. The callback calls render(null, scratch) which unmounts the tree // → options.unmount sets component.__hooks = undefined // 4. Resetting the hooks array to an empty array would throw an error let setVal; function App() { const [val, _setVal] = useState(0); setVal = _setVal; useEffect(() => { if (val === 1) { render(null, scratch); } }, [val]); return
    val: {val}
    ; } act(() => { render(, scratch); }); act(() => { setVal(1); }); }); it('should not rerun when receiving NaN on subsequent renders', () => { const calls = []; const Component = ({ value }) => { const [count, setCount] = useState(0); useEffect(() => { calls.push('doing effect' + count); setCount(count + 1); return () => { calls.push('cleaning up' + count); }; }, [value]); return

    {count}

    ; }; const App = () => ; act(() => { render(, scratch); }); expect(calls.length).to.equal(1); expect(calls).to.deep.equal(['doing effect0']); }); }); ================================================ FILE: hooks/test/browser/useEffectAssertions.jsx ================================================ import { setupRerender } from 'preact/test-utils'; import { createElement, render } from 'preact'; import { setupScratch, teardown } from '../../../test/_util/helpers'; import { vi } from 'vitest'; // Common behaviors between all effect hooks export function useEffectAssertions(useEffect, scheduleEffectAssert) { /** @type {HTMLDivElement} */ let scratch; /** @type {() => void} */ let rerender; beforeEach(() => { scratch = setupScratch(); rerender = setupRerender(); }); afterEach(() => { teardown(scratch); }); it('performs the effect after every render by default', () => { const callback = vi.fn(); function Comp() { useEffect(callback); return null; } render(, scratch); return scheduleEffectAssert(() => expect(callback).toHaveBeenCalledOnce()) .then(() => scheduleEffectAssert(() => expect(callback).toHaveBeenCalledOnce()) ) .then(() => render(, scratch)) .then(() => scheduleEffectAssert(() => expect(callback).toHaveBeenCalledTimes(2)) ); }); it('performs the effect only if one of the inputs changed', () => { const callback = vi.fn(); function Comp(props) { useEffect(callback, [props.a, props.b]); return null; } render(, scratch); return scheduleEffectAssert(() => expect(callback).toHaveBeenCalledOnce()) .then(() => render(, scratch)) .then(() => scheduleEffectAssert(() => expect(callback).toHaveBeenCalledOnce()) ) .then(() => render(, scratch)) .then(() => scheduleEffectAssert(() => expect(callback).toHaveBeenCalledTimes(2)) ) .then(() => render(, scratch)) .then(() => scheduleEffectAssert(() => expect(callback).toHaveBeenCalledTimes(2)) ); }); it('performs the effect at mount time and never again if an empty input Array is passed', () => { const callback = vi.fn(); function Comp() { useEffect(callback, []); return null; } render(, scratch); render(, scratch); expect(callback).toHaveBeenCalledOnce(); return scheduleEffectAssert(() => expect(callback).toHaveBeenCalledOnce()) .then(() => render(, scratch)) .then(() => scheduleEffectAssert(() => expect(callback).toHaveBeenCalledOnce()) ); }); it('calls the cleanup function followed by the effect after each render', () => { const cleanupFunction = vi.fn(); const callback = vi.fn(() => cleanupFunction); function Comp() { useEffect(callback); return null; } render(, scratch); return scheduleEffectAssert(() => { expect(cleanupFunction).not.toHaveBeenCalled(); expect(callback).toHaveBeenCalledOnce(); }) .then(() => scheduleEffectAssert(() => expect(callback).toHaveBeenCalledOnce()) ) .then(() => render(, scratch)) .then(() => scheduleEffectAssert(() => { expect(cleanupFunction).toHaveBeenCalledOnce(); expect(callback).toHaveBeenCalledTimes(2); const callbackLastInvocation = callback.mock.invocationCallOrder[ callback.mock.invocationCallOrder.length - 1 ]; expect(callbackLastInvocation).to.be.greaterThan( cleanupFunction.mock.invocationCallOrder[0] ); }) ); }); it('cleanups the effect when the component get unmounted if the effect was called before', () => { const cleanupFunction = vi.fn(); const callback = vi.fn(() => cleanupFunction); function Comp() { useEffect(callback); return null; } render(, scratch); return scheduleEffectAssert(() => { render(null, scratch); rerender(); expect(cleanupFunction).toHaveBeenCalledOnce(); }); }); it('works with closure effect callbacks capturing props', () => { const values = []; function Comp(props) { useEffect(() => values.push(props.value)); return null; } render(, scratch); render(, scratch); return scheduleEffectAssert(() => expect(values).to.deep.equal([1, 2])); }); } ================================================ FILE: hooks/test/browser/useId.test.jsx ================================================ import { createElement, Fragment, hydrate, render } from 'preact'; import { useId, useReducer, useState } from 'preact/hooks'; import { setupRerender } from 'preact/test-utils'; import { render as rts } from 'preact-render-to-string'; import { setupScratch, teardown } from '../../../test/_util/helpers'; describe('useId', () => { /** @type {HTMLDivElement} */ let scratch, rerender; beforeEach(() => { scratch = setupScratch(); rerender = setupRerender(); }); afterEach(() => { teardown(scratch); }); it('keeps the id consistent after an update', () => { function Comp() { const id = useId(); return
    ; } render(, scratch); const id = scratch.firstChild.getAttribute('id'); expect(scratch.firstChild.getAttribute('id')).to.equal(id); render(, scratch); expect(scratch.firstChild.getAttribute('id')).to.equal(id); }); it('ids are unique according to dom-depth', () => { function Child() { const id = useId(); const spanId = useId(); return (
    h
    ); } function Comp() { const id = useId(); return (
    ); } render(, scratch); expect(scratch.innerHTML).to.equal( '
    h
    ' ); render(, scratch); expect(scratch.innerHTML).to.equal( '
    h
    ' ); }); it('ids are unique across siblings', () => { function Child() { const id = useId(); return h; } function Comp() { const id = useId(); return (
    ); } render(, scratch); expect(scratch.innerHTML).to.equal( '
    hhh
    ' ); render(, scratch); expect(scratch.innerHTML).to.equal( '
    hhh
    ' ); }); it('correctly handles new elements', () => { let set; function Child() { const id = useId(); return h; } function Stateful() { const [state, setState] = useState(false); set = setState; return (
    {state && }
    ); } function Comp() { const id = useId(); return (
    ); } render(, scratch); expect(scratch.innerHTML).to.equal( '
    h
    ' ); set(true); rerender(); expect(scratch.innerHTML).to.equal( '
    hh
    ' ); }); it('matches with rts', () => { const ChildFragmentReturn = ({ children }) => { return {children}; }; const ChildReturn = ({ children }) => { return children; }; const SomeMessage = ({ msg }) => { const id = useId(); return (

    {msg} {id}

    ); }; const Stateful = () => { const [count, add] = useReducer(c => c + 1, 0); const id = useId(); return (
    id: {id}, count: {count}
    ); }; const Component = ({ showStateful = false }) => { const rootId = useId(); const paragraphId = useId(); return (
    ID: {rootId}

    Hello world id: {paragraphId}

    {showStateful ? : }
    ); }; const rtsOutput = rts(); render(, scratch); expect(rtsOutput === scratch.innerHTML).to.equal(true); }); it('matches with rts after hydration', () => { const ChildFragmentReturn = ({ children }) => { return {children}; }; const ChildReturn = ({ children }) => { return children; }; const SomeMessage = ({ msg }) => { const id = useId(); return (

    {msg} {id}

    ); }; const Stateful = () => { const [count, add] = useReducer(c => c + 1, 0); const id = useId(); return (
    id: {id}, count: {count}
    ); }; const Component = ({ showStateful = false }) => { const rootId = useId(); const paragraphId = useId(); return (
    ID: {rootId}

    Hello world id: {paragraphId}

    {showStateful ? : }
    ); }; const rtsOutput = rts(); scratch.innerHTML = rtsOutput; hydrate(, scratch); expect(rtsOutput).to.equal(scratch.innerHTML); }); it('should be unique across Fragments', () => { const ids = []; function Foo() { const id = useId(); ids.push(id); return

    {id}

    ; } function App() { return (
    ); } render(, scratch); expect(ids[0]).not.to.equal(ids[1]); }); it('should match implicite Fragments with RTS', () => { function Foo() { const id = useId(); return

    {id}

    ; } function Bar(props) { return props.children; } function App() { return ( ); } const rtsOutput = rts(); scratch.innerHTML = rtsOutput; hydrate(, scratch); expect(rtsOutput).to.equal(scratch.innerHTML); }); it('should skip component top level Fragment child', () => { const Wrapper = ({ children }) => { return {children}; }; const ids = []; function Foo() { const id = useId(); ids.push(id); return

    {id}

    ; } function App() { const id = useId(); ids.push(id); return (

    {id}

    ); } render(, scratch); expect(ids[0]).not.to.equal(ids[1]); }); it('should skip over HTML', () => { const ids = []; function Foo() { const id = useId(); ids.push(id); return

    {id}

    ; } function App() { return (
    ); } render(, scratch); expect(ids[0]).not.to.equal(ids[1]); }); it('should reset for each renderToString roots', () => { const ids = []; function Foo() { const id = useId(); ids.push(id); return

    {id}

    ; } function App() { return (
    ); } const res1 = rts(); const res2 = rts(); expect(res1).to.equal(res2); }); it('should work with conditional components', () => { function Foo() { const id = useId(); return

    {id}

    ; } function Bar() { const id = useId(); return

    {id}

    ; } let update; function App() { const [v, setV] = useState(false); update = setV; return
    {!v ? : }
    ; } render(, scratch); const first = scratch.innerHTML; update(v => !v); rerender(); expect(first).not.to.equal(scratch.innerHTML); }); it('should return a unique id across invocations of render', () => { const Id = () => { const id = useId(); return
    My id is {id}
    ; }; const App = props => { return (
    {props.secondId ? : null}
    ); }; render(createElement(App, { secondId: false }), scratch); expect(scratch.innerHTML).to.equal('
    My id is P0-0
    '); render(createElement(App, { secondId: true }), scratch); expect(scratch.innerHTML).to.equal( '
    My id is P0-0
    My id is P0-1
    ' ); }); it('should not crash for rendering null after a non-null render', () => { const Id = () => { const id = useId(); return
    My id is {id}
    ; }; const App = props => { return (
    {props.secondId ? : null}
    ); }; render(createElement(App, { secondId: false }), scratch); expect(scratch.innerHTML).to.equal('
    My id is P0-0
    '); render(null, scratch); expect(scratch.innerHTML).to.equal(''); }); }); ================================================ FILE: hooks/test/browser/useImperativeHandle.test.jsx ================================================ import { createElement, render } from 'preact'; import { setupScratch, teardown } from '../../../test/_util/helpers'; import { useImperativeHandle, useRef, useState } from 'preact/hooks'; import { setupRerender } from 'preact/test-utils'; import { vi } from 'vitest'; describe('useImperativeHandle', () => { /** @type {HTMLDivElement} */ let scratch; /** @type {() => void} */ let rerender; beforeEach(() => { scratch = setupScratch(); rerender = setupRerender(); }); afterEach(() => { teardown(scratch); }); it('Mutates given ref', () => { let ref; function Comp() { ref = useRef({}); useImperativeHandle(ref, () => ({ test: () => 'test' }), []); return

    Test

    ; } render(, scratch); expect(ref.current).to.have.property('test'); expect(ref.current.test()).to.equal('test'); }); it('Calls ref unmounting function', () => { let ref; const unmount = vi.fn(); function Comp() { useImperativeHandle( r => { ref = r; return unmount; }, () => ({ test: () => 'test' }), [] ); return

    Test

    ; } render(, scratch); expect(ref).to.have.property('test'); expect(ref.test()).to.equal('test'); render(null, scratch); expect(unmount).toHaveBeenCalledOnce(); expect(ref).to.equal(null); }); it('calls createHandle after every render by default', () => { let ref, createHandleSpy = vi.fn(); function Comp() { ref = useRef({}); useImperativeHandle(ref, createHandleSpy); return

    Test

    ; } render(, scratch); expect(createHandleSpy).toHaveBeenCalledOnce(); render(, scratch); expect(createHandleSpy).toHaveBeenCalledTimes(2); render(, scratch); expect(createHandleSpy).toHaveBeenCalledTimes(3); }); it('calls createHandle only on mount if an empty array is passed', () => { let ref, createHandleSpy = vi.fn(); function Comp() { ref = useRef({}); useImperativeHandle(ref, createHandleSpy, []); return

    Test

    ; } render(, scratch); expect(createHandleSpy).toHaveBeenCalledOnce(); render(, scratch); expect(createHandleSpy).toHaveBeenCalledOnce(); }); it('Updates given ref when args change', () => { let ref, createHandleSpy = vi.fn(); function Comp({ a }) { ref = useRef({}); useImperativeHandle(ref, () => { createHandleSpy(); return { test: () => 'test' + a }; }, [a]); return

    Test

    ; } render(, scratch); expect(createHandleSpy).toHaveBeenCalledOnce(); expect(ref.current).to.have.property('test'); expect(ref.current.test()).to.equal('test0'); render(, scratch); expect(createHandleSpy).toHaveBeenCalledTimes(2); expect(ref.current).to.have.property('test'); expect(ref.current.test()).to.equal('test1'); render(, scratch); expect(createHandleSpy).toHaveBeenCalledTimes(3); expect(ref.current).to.have.property('test'); expect(ref.current.test()).to.equal('test0'); }); it('Updates given ref when passed-in ref changes', () => { let ref1, ref2; /** @type {(arg: any) => void} */ let setRef; /** @type {() => void} */ let updateState; const createHandleSpy = vi.fn(() => ({ test: () => 'test' })); function Comp() { ref1 = useRef({}); ref2 = useRef({}); const [ref, setRefInternal] = useState(ref1); setRef = setRefInternal; let [value, setState] = useState(0); updateState = () => setState((value + 1) % 2); useImperativeHandle(ref, createHandleSpy, []); return

    Test

    ; } render(, scratch); expect(createHandleSpy).toHaveBeenCalledOnce(); updateState(); rerender(); expect(createHandleSpy).toHaveBeenCalledOnce(); setRef(ref2); rerender(); expect(createHandleSpy).toHaveBeenCalledTimes(2); updateState(); rerender(); expect(createHandleSpy).toHaveBeenCalledTimes(2); setRef(ref1); rerender(); expect(createHandleSpy).toHaveBeenCalledTimes(3); }); it('should not update ref when args have not changed', () => { let ref, createHandleSpy = vi.fn(() => ({ test: () => 'test' })); function Comp() { ref = useRef({}); useImperativeHandle(ref, createHandleSpy, [1]); return

    Test

    ; } render(, scratch); expect(createHandleSpy).toHaveBeenCalledOnce(); expect(ref.current.test()).to.equal('test'); render(, scratch); expect(createHandleSpy).toHaveBeenCalledOnce(); expect(ref.current.test()).to.equal('test'); }); it('should not throw with nullish ref', () => { function Comp() { useImperativeHandle(null, () => ({ test: () => 'test' }), [1]); return

    Test

    ; } expect(() => render(, scratch)).to.not.throw(); }); it('should reset ref object to null when the component get unmounted', () => { let ref, createHandleSpy = vi.fn(() => ({ test: () => 'test' })); function Comp() { ref = useRef({}); useImperativeHandle(ref, createHandleSpy, [1]); return

    Test

    ; } render(, scratch); expect(createHandleSpy).toHaveBeenCalledOnce(); expect(ref.current).to.not.equal(null); render(
    , scratch); expect(createHandleSpy).toHaveBeenCalledOnce(); expect(ref.current).to.equal(null); }); it('should reset ref callback to null when the component get unmounted', () => { const ref = vi.fn(); const handle = { test: () => 'test' }; const createHandleSpy = vi.fn(() => handle); function Comp() { useImperativeHandle(ref, createHandleSpy, [1]); return

    Test

    ; } render(, scratch); expect(createHandleSpy).toHaveBeenCalledOnce(); expect(ref).toHaveBeenCalledWith(handle); ref.mockClear(); render(
    , scratch); expect(createHandleSpy).toHaveBeenCalledOnce(); expect(ref).toHaveBeenCalledWith(null); }); }); ================================================ FILE: hooks/test/browser/useLayoutEffect.test.jsx ================================================ import { act } from 'preact/test-utils'; import { createElement, render, Fragment, Component } from 'preact'; import { setupScratch, teardown, serializeHtml } from '../../../test/_util/helpers'; import { useEffectAssertions } from './useEffectAssertions'; import { useLayoutEffect, useRef, useState } from 'preact/hooks'; import { vi } from 'vitest'; describe('useLayoutEffect', () => { /** @type {HTMLDivElement} */ let scratch; beforeEach(() => { scratch = setupScratch(); }); afterEach(() => { teardown(scratch); }); // Layout effects fire synchronously const scheduleEffectAssert = assertFn => new Promise(resolve => { assertFn(); resolve(); }); useEffectAssertions(useLayoutEffect, scheduleEffectAssert); it('calls the effect immediately after render', () => { const cleanupFunction = vi.fn(); const callback = vi.fn(() => cleanupFunction); function Comp() { useLayoutEffect(callback); return null; } render(, scratch); render(, scratch); expect(cleanupFunction).toHaveBeenCalledOnce(); expect(callback).toHaveBeenCalledTimes(2); render(, scratch); expect(cleanupFunction).toHaveBeenCalledTimes(2); expect(callback).toHaveBeenCalledTimes(3); }); it('works on a nested component', () => { const callback = vi.fn(); function Parent() { return (
    ); } function Child() { useLayoutEffect(callback); return null; } render(, scratch); expect(callback).toHaveBeenCalledOnce(); }); it('should execute multiple layout effects in same component in the right order', () => { let executionOrder = []; const App = ({ i }) => { executionOrder = []; useLayoutEffect(() => { executionOrder.push('action1'); return () => executionOrder.push('cleanup1'); }, [i]); useLayoutEffect(() => { executionOrder.push('action2'); return () => executionOrder.push('cleanup2'); }, [i]); return

    Test

    ; }; render(, scratch); render(, scratch); expect(executionOrder).to.deep.equal([ 'cleanup1', 'cleanup2', 'action1', 'action2' ]); }); it('should correctly display DOM', () => { function AutoResizeTextareaLayoutEffect(props) { const ref = useRef(null); useLayoutEffect(() => { // IE & Edge put textarea's value as child of textarea when reading innerHTML so use // cross browser serialize helper const actualHtml = serializeHtml(scratch); const expectedHTML = `

    ${props.value}

    `; expect(actualHtml).to.equal(expectedHTML); expect(document.body.contains(ref.current)).to.equal(true); }); return (

    {props.value}

    Stats