mrkeyoor.com_
Sat 08 Aug 22:48 UTC
npmWeb Frontendupdated 08 Aug 2026

react-universal-interface

react-universal-interface is a tiny compatibility layer for React component authors who want one data-providing component to accept several older composition styles. Its render helper can feed the same data to a function child, a render prop, a component prop, or an existing custom React element. It also builds higher-order components and can wrap a hook in a render-prop component. This is infrastructure for library APIs, not a visual component or a state manager, and its design reflects the React 16 render-prop and HOC era.

Verdict

Keep it only when you must preserve an existing universal-interface contract. For new React code, hooks plus a single explicit composition style are easier to type, explain, and maintain.

API stability3/5The shipped surface is only render, createEnhancer, hookToRenderProp, and a UniversalProps type, and version 0.6.2 has not changed since May 2020. That long quiet period makes existing behavior unlikely to move, but it is stagnation rather than a stated compatibility promise. The react peer range is '*', while the package's own development setup used React 16.8, so stability on current React releases is not demonstrated by its tests or documentation.
Docs2/5The README clearly demonstrates render props, function children, component props, element injection, and basic enhancer creation, then gives signatures for the two original helpers. It does not document the shipped hookToRenderProp export, precedence when children and render coexist, host-element behavior, data-overwrites-props behavior, CommonJS packaging, or the peer dependency on tslib. Those omissions cover most of the mistakes a current adopter is likely to make.
Maintenance1/5The last runtime change and npm release were both in May 2020. Repository activity after that consists of a license-field change and a security-policy file, and the license change still has not been published in 0.6.2. An open issue created in October 2025 asks for a release. The repository is not archived, but there is no evidence of active runtime maintenance, current React testing, or a supported release cadence.
Ecosystem2/5The package recorded 3,241,273 downloads in the measured npm week, but that reach is likely driven heavily by transitive use in older React libraries. The repository has 39 stars and one open issue, and the README's public-interface badge never became a broad React convention. Modern React documentation and libraries generally standardize on hooks, Context, or a focused asChild pattern instead of exposing every render-prop and HOC shape together.

Use it if

  • You maintain an existing component library that already promises function-child, render-prop, component-prop, and element-injection forms from one component
  • You need to preserve a legacy render-prop or HOC public API while moving its implementation to hooks
  • You want a small helper that normalizes several React 16-era composition conventions without adding its own state model
  • You have verified the behavior against your current React version and can own the compatibility risk of an old, lightly maintained package
Skip it if

Setup reality

Install react-universal-interface together with its two peers, react and tslib. The package declares both peers as wildcards, so npm will not protect you from pairing a 2020 build with a much newer React release. There is no configuration file or build step, and TypeScript declarations ship in the package, but the declarations were produced with TypeScript 3.4 and React 16-era types. The published entry point is CommonJS only; there is no module or exports field, so modern bundlers must interoperate with lib/index.js rather than selecting a native ESM build. The main surprise is behavioral, not installation-related. Function children win when both children and render are provided, with only a development warning. A custom component element is cloned and receives the data object as props, but a host element such as div is returned unchanged. The component and comp forms receive the data object, not arbitrary provider props. render also expects data to be an object and warns in development for primitives, although it still calls the callback. hookToRenderProp is present in the shipped exports but barely documented in the README: by default it calls your hook with the wrapper's entire props object as one argument, so hooks with zero or positional arguments need an explicit mapping function. Finally, repository metadata says Unlicense while npm 0.6.2 omits the license field; teams with automated policy checks may need an exception or should avoid the package.

Patterns

Expose data through a function childsupport-function-child

import React from 'react';
import { render } from 'react-universal-interface';

function MousePosition(props) {
  const data = { x: 20, y: 40 };
  return render(props, data);
}

const view = (
  <MousePosition>
    {({ x, y }) => <output>{x}, {y}</output>}
  </MousePosition>
);

A function in children takes precedence over the render prop. Supplying both logs a warning in development and uses children.

Expose the same data through a render propsupport-render-prop

function MousePosition(props) {
  return render(props, { x: 20, y: 40 });
}

<MousePosition
  render={({ x, y }) => <output>{x}, {y}</output>}
/>;

The render prop is used only when children is undefined; even a non-function children value replaces it.

Render a caller-supplied componentsupport-component-prop

const PositionLabel = ({ x, y }) => <output>{x}, {y}</output>;

function MousePosition(props) {
  return render(props, { x: 20, y: 40 });
}

<MousePosition component={PositionLabel} />;

The supplied component receives the data object as its props. Other props passed to MousePosition are not forwarded by render.

Use the shorter comp aliasuse-comp-alias

<MousePosition comp={PositionLabel} />;

comp is an alias for component and wins if both are present, which can hide a caller mistake instead of throwing.

Inject data into an existing custom elementinject-custom-element

const PositionLabel = ({ x, y, tone }) => (
  <output data-tone={tone}>{x}, {y}</output>
);

<MousePosition>
  <PositionLabel x={0} y={0} tone="quiet" />
</MousePosition>;

render clones a custom component element and merges data after its existing props, so data x and y overwrite the element's x and y values.

Pass actions after the data argumentpass-extra-callback-arguments

function Counter(props) {
  const data = { count: 2 };
  const actions = { increment: () => console.log('increment') };
  return render(props, data, actions);
}

<Counter>
  {({ count }, { increment }) => (
    <button onClick={increment}>{count}</button>
  )}
</Counter>;

Extra arguments are forwarded only to a function child or render callback. Component and element forms receive only the data object.

Create an HOC with data under one propcreate-named-enhancer

import { createEnhancer, render } from 'react-universal-interface';

function MousePosition(props) {
  return render(props, { x: 20, y: 40 });
}

const withMouse = createEnhancer(MousePosition, 'mouse');
const Label = ({ mouse }) => <output>{mouse.x}, {mouse.y}</output>;
const LabelWithMouse = withMouse(Label);

Passing a prop name nests provider data under that prop and keeps the wrapped component's original props beside it.

Spread provider data into wrapped propscreate-spread-enhancer

const withMouseProps = createEnhancer(MousePosition);
const Label = ({ x, y, prefix }) => (
  <output>{prefix}: {x}, {y}</output>
);
const Enhanced = withMouseProps(Label);

<Enhanced prefix="cursor" />;

Without a prop name, provider data is spread first and the wrapped component's original props are spread afterward, so original props win on name collisions.

Pass fixed props to the provider componentconfigure-enhancer-provider

const withRemoteUser = createEnhancer(UserProvider, 'user');
const UserCard = ({ user }) => <strong>{user.name}</strong>;

const AdminCard = withRemoteUser(UserCard, 'user', {
  endpoint: '/api/admin',
});

The third enhancer argument is passed to the provider, not the wrapped component. It is fixed when the enhanced component is created.

Turn a zero-argument hook into a render-prop componentwrap-zero-argument-hook

import { hookToRenderProp } from 'react-universal-interface';

function useOnlineStatus() {
  return { online: navigator.onLine };
}

const OnlineStatus = hookToRenderProp(useOnlineStatus, () => []);

<OnlineStatus>
  {({ online }) => <span>{online ? 'online' : 'offline'}</span>}
</OnlineStatus>;

Supply () => [] for a zero-argument hook. The default mapper passes the wrapper's entire props object as the hook's first argument.

Map component props to positional hook argumentsmap-hook-arguments

function useUser(userId) {
  return { id: userId, name: 'Ada' };
}

const User = hookToRenderProp(
  useUser,
  (props) => [props.userId]
);

<User userId="42" render={(user) => <strong>{user.name}</strong>} />;

The mapper must return an array because its entries are spread into the hook call as positional arguments.

Type a provider component's supported render formstype-universal-props

import React from 'react';
import { render, UniversalProps } from 'react-universal-interface';

type Position = { x: number; y: number };
type Props = UniversalProps<Position> & { source: string };

function MousePosition(props: Props) {
  const data: Position = { x: 20, y: 40 };
  return render(props, data);
}

UniversalProps describes children, render, comp, and component, but render itself accepts any props and data, so it does not enforce the relationship at the helper call.

Alternatives

PackageRegistryPick it when
reactnpmUse built-in hooks, Context, and ordinary children for a new application or component API with fewer conventions
@radix-ui/react-slotnpmUse the asChild composition pattern when your real need is merging behavior into one caller-supplied element
react-trackednpmUse tracked Context or hook state when the goal is selective subscriptions and fewer state-driven rerenders