Skip to content

recrafter-group/ctx

Repository files navigation

@recrafter/ctx

A simple wrapper around React.createContext that makes it easy to use hooks and context together. It reduces boilerplate and improves type safety, providing a clean and developer-friendly API.

Contents

Features

  • 🔄 Simple hook-based context creation with minimal boilerplate
  • 🛡️ Strict, automatic type inference out of the box
  • đź§© Render function support inside the provider for direct context access
  • ⚙️ Easy binding of context props with the value hook
  • đźš« No need for useContext(Context) or Context.Consumer — cleaner API

Requirements

  • React ≥ 16.8
  • TypeScript (optional, but recommended)

Installation

npm i react @recrafter/ctx

Usage

Basic Example

import React from 'react';
import {createCtx} from '@recrafter/ctx';

// Define a hook with your value:
const useValue = () => React.useState('');
// Context factory takes any hook as the first argument:
const Ctx = createCtx(useValue);

// Now you can use Ctx.use() to get context value:

const Input = () => {
    const [value, setValue] = Ctx.use();
    return <input value={value} onChange={(e) => setValue(e.target.value)} />;
};

const ResetButton = () => {
    const [, setValue] = Ctx.use();
    return <button onClick={() => setValue('')}>Reset</button>;
};

// Put Ctx component above in the component tree.
// The Ctx component is the place where the hook is executed.
const App = () => (
    <Ctx>
        <Input />
        <ResetButton />
    </Ctx>
);

Using Render Functions

You can also use a render function to get the context value:

const App = () => (
    <Ctx>
        {([value, setValue]) => (
            <>
                <input
                    value={value}
                    onChange={(e) => setValue(e.target.value)}
                />
                <button onClick={() => setValue('')}>Reset</button>
            </>
        )}
    </Ctx>
);

Passing Props to Context

You can pass properties to the context component:

interface CtxProps {
    initialValue: string;
}

const Ctx = createCtx((props: CtxProps) => {
    return React.useState(props.initialValue);
});

const App = () => (
    <Ctx initialValue="Hello, world!">
        <Input />
        <ResetButton />
    </Ctx>
);

Missing Provider Error Prevention

Unlike native useContext, this library prevents using Ctx.use() without declaring the Ctx component higher in the component tree. If you try to do this:

const Ctx = createCtx(() => React.useState(''), 'inputValue');

const Input = () => {
    const [value, setValue] = Ctx.use();
    return <input value={value} onChange={(e) => setValue(e.target.value)} />;
};

// MissingCtxProviderError: no provider in the component tree
const App = () => <Input />;

You'll get a clear error message:

Context provider is missing. Please mount Ctx(inputValue) component above in the component tree.

Using injections

The Ctx.inject() - HOC provides an alternative way to consume context values by injecting them directly into component props.

Direct injection

const useValue = () => {
    const [value, setValue] = React.useState('');
    return {value, setValue};
};
const Ctx = createCtx(useValue);

type InputProps = {
    value: string;
    setValue: (value: string) => void;
    type: 'password' | 'text';
};

const Input = ({type, value, setValue}: InputProps) => (
    <input
        type={type}
        value={value}
        onChange={(e) => setValue(e.target.value)}
    />
);

type ResetButtonProps = {setValue: (value: string) => void};

const ResetButton = ({setValue}: ResetButtonProps) => (
    <button onClick={() => setValue('')}>Reset</button>
);

// Inject context values directly into component props
const InjectedInput = Ctx.inject(Input);
const InjectedResetButton = Ctx.inject(ResetButton);

const App = () => (
    <Ctx>
        <InjectedInput type="text" />
        <InjectedResetButton />
    </Ctx>
);

Mapped injection

You can also use a mapper function to transform context values. The function can be a regular function or a React hook:

const useValue = () => React.useState('');
const Ctx = createCtx(useValue);

type InputProps = {
    value: string;
    setValue: (value: string) => void;
    type: 'password' | 'text';
};

const Input = ({type, value, setValue}: InputProps) => (
    <input
        type={type}
        value={value}
        onChange={(e) => setValue(e.target.value)}
    />
);

type ExtraProps = {wrapper: string};

const InjectedInput = Ctx.inject(
    ([value, setValue], {wrapper}: ExtraProps) => ({
        value: `${wrapper}${value}${wrapper}`,
        setValue,
    }),
    Input,
);

const App = () => (
    <Ctx>
        <InjectedInput type="text" wrapper="--" />
    </Ctx>
);

API

createCtx(useValue, displayName?)

Function to create a context.

Parameters:

  • useValue: Hook that returns the context value
  • displayName?: Optional name for debugging

Returns:

  • CtxComponent: Context component with use() and inject() methods

CtxComponent

Props:

  • children: React elements or a render function (value) => ReactNode
  • Any additional props that are passed to the useValue hook

Methods:

  • use(): Value: Hook to get the context value
  • inject(Component): InjectedComponent: Inject context value directly into component props
  • inject(useMappedValue, Component): InjectedComponent: Inject transformed context value using a mapper function, that can be a regular function or a React hook

Development

# Install dependencies
npm ci

# Run tests
npm run test

# Build the project
npm run build

# Check code format
npm run formatter:check
npm run formatter:fix

# Analyze code quality
npm run analyzer:code:check
npm run analyzer:code:fix

# Analyze public API quality
npm run analyzer:api:check

About

A simple wrapper around React.createContext that makes it easy to use hooks and context together

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published