mrkeyoor.com_
Sat 08 Aug 21:57 UTC
npmWeb Frontendupdated 08 Aug 2026

react-side-effect

react-side-effect is a small React higher-order component factory for turning props from every mounted instance of a component into one shared browser side effect. You supply a reducer, a client callback, and optionally a server mapper. It is the low-level mechanism behind patterns such as choosing the innermost document title or combining body styles. It predates hooks and keeps its mounted-instance list in the closure created for each wrapped component.

Verdict

Keep it when you are maintaining an established declarative side-effect component on React 18 or earlier. Do not choose it for a new React 19 or concurrent-rendering design; its legacy lifecycle and shared SSR cleanup contract are unnecessary risks today.

API stability4/5The public surface is only withSideEffect plus the generated component's peek and rewind methods, and that contract has changed very little. Stability here partly reflects age, however: the implementation still depends on UNSAFE_componentWillMount, and version 2.1.2 stops its declared React peer support at React 18 rather than adapting the API to current React behavior.
Docs3/5The README explains the reducer, client handler, optional server mapping, nesting example, peek, and the mandatory rewind cleanup with useful code. It does not document React Strict Mode or concurrent-rendering implications, TypeScript setup, ESM interop, ref behavior, or a migration path, even though open issues show lifecycle and React 19 concerns.
Maintenance2/5The repository is not archived and npm received version 2.1.2 in June 2022, but GitHub reports the last push in March 2023. Open reports include the legacy lifecycle and React 19 peer warning, while the source and development tests still target older React-era tools. That is enough activity to avoid calling it abandoned, but not enough for a foundational new dependency.
Ecosystem3/5The package recorded 3,510,865 downloads in the measured week and is used transitively by established React head-management packages. Its direct ecosystem is narrow: there are no runtime dependencies, no included types, no plugin system, and the HOC pattern has largely yielded to hooks or purpose-built providers for new React applications.

Use it if

  • You maintain an existing React 16, 17, or 18 component library that already exposes a declarative side-effect component
  • You need to aggregate props from several mounted component instances before changing one browser-global value
  • You need the same tiny abstraction to expose collected state after renderToString on a strictly sequential server renderer
  • You are repairing a package built on this exact HOC and replacing it would change a public API
Skip it if

Setup reality

Installation is only npm install react-side-effect, with no runtime dependencies, native build, credentials, or configuration file. The easy install hides the integration work. Version 2.1.2 declares React ^16.3, ^17, or ^18 as a peer, so npm can reject or warn in a React 19 tree. The package ships CommonJS and no TypeScript declarations; TypeScript projects normally add @types/react-side-effect and may need esModuleInterop for a default import. You must create a wrapper component, decide how every mounted instance is reduced, decide what an empty list means, and undo stale browser state when the last instance unmounts. The implementation uses PureComponent, UNSAFE_componentWillMount, and a closure-level array of instances. That legacy lifecycle is a serious fit concern for modern concurrent React. Server rendering has a separate operational rule: call the generated component's rewind() immediately after every renderToString, preferably in a finally block. The README explicitly warns that missing the call leaves the stack growing and returns incorrect state on later requests. peek() does not clear anything and is intended for tests. The client callback never runs on the server, while the optional third mapper changes what rewind returns. There is no built-in precedence model beyond the order your reducer receives, so nested priority, merging, defaults, and cleanup are all application code that needs tests.

Patterns

Create a declarative document title componentcreate-document-title

import React from 'react';
import withSideEffect from 'react-side-effect';

function TitleMarker() {
  return null;
}

const reduceTitles = (propsList) =>
  propsList.length ? propsList[propsList.length - 1].title : '';

export const DocumentTitle = withSideEffect(
  reduceTitles,
  (title) => { document.title = title; }
)(TitleMarker);

The last mounted instance wins, which often resembles innermost precedence but is based on registration order rather than React tree inspection.

Set a title from a screen componentrender-title-marker

function AccountScreen() {
  return (
    <>
      <DocumentTitle title="Account settings" />
      <main>...</main>
    </>
  );
}

The wrapped marker may render null; it does not need to wrap the page content.

Merge body styles from mounted markersmerge-body-styles

function BodyStyleMarker() { return null; }

export const BodyStyle = withSideEffect(
  (items) => Object.assign({}, ...items.map((item) => item.style)),
  (style) => {
    document.body.style.backgroundColor = style.backgroundColor || '';
    document.body.style.overflow = style.overflow || '';
  }
)(BodyStyleMarker);

Explicitly reset each property when no marker supplies it, or a style from an unmounted screen remains on document.body.

Toggle one global body classtoggle-body-class

function BodyClassMarker() { return null; }

export const BodyClass = withSideEffect(
  (items) => items.some((item) => item.modalOpen),
  (modalOpen) => {
    document.body.classList.toggle('has-modal', modalOpen);
  }
)(BodyClassMarker);

Use classList.toggle with its boolean argument so unmounting the final marker performs cleanup.

Make precedence explicit with a priority propchoose-highest-priority

const reduceByPriority = (items) => {
  const selected = items.reduce(
    (best, item) => !best || item.priority > best.priority ? item : best,
    null
  );
  return selected?.value ?? null;
};

Do this when correctness depends on precedence. Mounted-instance order is not a reliable substitute for a declared priority.

Collect unique values across every markercollect-unique-values

const reduceFlags = (items) => [
  ...new Set(items.flatMap((item) => item.flags || [])),
];

const FeatureFlags = withSideEffect(
  reduceFlags,
  (flags) => window.analytics?.setEnabledFlags(flags)
)(() => null);

The reducer runs on mount, update, and unmount, so keep it deterministic and inexpensive.

Read and clear state after server renderingextract-server-state

let title;
try {
  const html = renderToString(<App />);
  title = DocumentTitle.rewind();
  return renderPage({ html, title });
} finally {
  DocumentTitle.rewind();
}

rewind is server-only and clears the instance stack. The final cleanup protects error paths, but concurrent requests can still share the same closure state.

Return a server-specific representationmap-server-state

const ServerAwareTitle = withSideEffect(
  (items) => items.at(-1)?.title || '',
  (title) => { document.title = title; },
  (title) => ({ title, escaped: escapeHtml(title) })
)(() => null);

const titleState = ServerAwareTitle.rewind();

The optional third function runs only when the library decides no DOM is available; rewind returns its mapped value.

Inspect aggregated state without clearing itinspect-state-in-test

const view = mount(
  <>
    <DocumentTitle title="Outer" />
    <DocumentTitle title="Inner" />
  </>
);

expect(DocumentTitle.peek()).to.equal('Inner');
view.unmount();

The README reserves peek for tests. It does not reset the mounted-instance list, so unmount your test tree.

Reset server-mode state between testsreset-test-state

afterEach(() => {
  if (!DocumentTitle.canUseDOM) {
    DocumentTitle.rewind();
  }
});

rewind throws in a DOM environment. In browser-like tests, unmount rendered trees instead of calling it.

Load the CommonJS builduse-commonjs

const withSideEffect = require('react-side-effect');

const VisibilityMarker = withSideEffect(
  (items) => items.some((item) => item.hidden),
  (hidden) => { document.hiddenStateForApp = hidden; }
)(() => null);

The package metadata publishes a CommonJS main entry and no module entry.

Add community TypeScript declarationsadd-typescript-types

npm install react-side-effect
npm install --save-dev @types/react-side-effect

Version 2.1.2 does not include its own types. Check that the separate @types package matches the API you use.

Alternatives

PackageRegistryPick it when
react-helmet-asyncnpmUse it for document title, meta, link, and script tags with request-scoped server rendering.
react-clientside-effectnpmUse it when you need a closely related client-oriented HOC with newer lifecycle choices, after checking its own React compatibility.
@react-hookz/webnpmUse it when one component can own the browser effect and a maintained hook is clearer than tree-wide prop aggregation.