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.
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.
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
- You are designing a new application API: ordinary hooks and Context are clearer than offering four interchangeable rendering conventions, and React itself already supplies those primitives
- You need evidence of current React support: version 0.6.2 was tested with React 16.8-era tooling, while its peer dependency is the unbounded react wildcard and the README makes no React 18 or 19 claims
- You expect active maintenance: the last runtime change and npm release were in May 2020; later repository commits only added license and security-policy files, and an open issue asks for a new release
- Your compliance tooling requires declared package metadata: the shipped 0.6.2 package.json has no license field even though the tarball contains an Unlicense file, and the repository fix has not reached npm
- You want predictable prop flow: render-prop callbacks receive data plus optional extra arguments, component props receive only the data object, and custom element injection lets data overwrite same-named existing props
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
| Package | Registry | Pick it when |
|---|---|---|
| react | npm | Use built-in hooks, Context, and ordinary children for a new application or component API with fewer conventions |
| @radix-ui/react-slot | npm | Use the asChild composition pattern when your real need is merging behavior into one caller-supplied element |
| react-tracked | npm | Use tracked Context or hook state when the goal is selective subscriptions and fewer state-driven rerenders |