Skip to content

Contributing to react-simplikit

react-simplikit is designed to encourage contributions from anyone. If you'd like to contribute, please follow the guide below.

Scope

react-simplikit provides hooks, components and utilities that work in every React environment — browser, server rendering and React Native — plus hooks that solve problems specific to browsers and mobile web browsers, such as the on-screen keyboard, safe-area insets and the visual viewport. Anything that interferes with React's lifecycle or depends on another library is out of scope; see the Design Principles.

Source lives under packages/react-simplikit/src in hooks/, components/ and utils/.

Implementation Contribution

When contributing implementations, add them to the appropriate directory based on their type (components, hooks, or utils). Each implementation must include the following elements:

  • Implementation
  • Test Code
  • JSDoc

TIP

Do I need to write documentation?

No, you don't need to write documentation separately. Instead, please write detailed JSDoc comments, then run yarn docs:gen <name> to generate the English documentation from them and commit the result with your PR. Translations are maintained separately; until one exists, the page is shown in English with a notice.

Writing Implementations

You must follow react-simplikit's Design Principles. We don't provide implementations that depend on specific libraries or are tightly coupled with React's lifecycle. Please write implementations that adhere to these design principles.

Writing JSDoc

All implementations must include JSDoc comments. These provide hints when using the implementation and play a crucial role in generating documentation. JSDoc comments must include @description and @example, and if there are parameters or return values, they should include @param and @returns.

JSDoc writing rules must be followed for accurate documentation generation. If JSDoc validation fails, CI might fail.
  • JSDoc must be written in English.

  • @description: A required tag that clearly explains the implementation's functionality or role.

  • @example: A required tag that shows example code demonstrating how to use the implementation.

  • @param: Write the parameter's name and description. Must be included if the implementation has parameters.

    • For required parameters: @param {<type>} <parameter name> - <parameter description>

    • For optional parameters: @param {<type>} [<parameter name>] - <parameter description>

    • For object parameters, both the object itself and its properties need @param tags.

    • If you want to write a list under a description, use -- instead of -.

      ts
      type Props = {
        name: string;
        age: number;
        nickname?: string;
        company: {
          name: string;
          address?: string;
        };
        paymentMethod?: {
          type: 'card' | 'account';
          number?: string;
        };
      };
      
      /**
       * @param {string} name - Name of the user.
       * @param {number} age - Age of the user.
       * @param {string} [nickname] - Nickname of the user.
       * @param {Object} company - Company information of the user.
       * @param {string} company.name - Name of the company.
       * @param {string} [company.address] - Address of the company.
       * @param {Object} [paymentMethod] - Payment information of the user.
       * @param {string} [paymentMethod.type] - Payment method.
       * @param {string} [paymentMethod.number] - Card or account number.
       *   -- Card or account number without `-`.
       *   -- If the number is a card number, it should be 15 or 16 digits.
       */

      This JSDoc will be converted into the following documentation.

      • namerequired · string

        Name of the user.

      • agerequired · number

        Age of the user.

      • nicknamestring

        Nickname of the user.

      • companyrequired · Object

        Company information of the user.

        • company.namerequired · string

          Name of the company.

        • company.addressstring

          Address of the company.

      • paymentMethodObject

        Payment information of the user.

        • paymentMethod.typerequired · string

          Payment method.

        • paymentMethod.numberstring

          Card or account number.
          - Card or account number without `-`.
          - If the number is a card number, it should be 15 or 16 digits.

  • @returns: Write the return value's name and description. Must be included if the implementation has return values.

    • Format: @returns {<type>} <return value description>

    • For object or tuple return values, include descriptions for each member.

    • If additional details are needed for each member, please use :.

      ts
      type ReturnValue = [Object, () => void];
      
      /**
       * @returns {[Object, () => void]} A tuple containing:
       * - obj `Object` - An object containing:
       *   : label `string` - The label of the input.
       *   : value `string` - The value of the input.
       * - onChange `() => void` - A function to update the value.
       */

      This JSDoc will be converted into the following documentation.

      • [obj: Object, onChange: () => void]

        A tuple containing:

        • objObject

          An object containing:
          : label string - The label of the input.
          : value string - The value of the input.

        • onChange() => void

          A function to update the value.


      Object-type return values can be written in a similar way.

      ts
      type ReturnValue = { value: string; onChange: () => void };
      
      /**
       * @returns {Object} An object containing:
       * - value `string` - The value of the input.
       * - onChange `() => void` - A function to update the value.
       */

      This JSDoc will be converted into the following documentation.

      • Object

        An object containing:

        • valuestring

          The value of the input.

        • onChange() => void

          A function to update the value.

Writing Test Code

All implementations must include test code, written with the same name as the implementation. Test coverage must always reach 100%. Use the following command to verify coverage:

bash
yarn test:coverage
Please verify safe operation in SSR environments

All react-simplikit implementations use special rendering functions to verify safe operation in SSR environments.

  • Component Testing

    tsx
    it('is safe on server side rendering', () => {
      // renderSSR.serverOnly is a method that renders the component in the server environment.
      // In this environment, hooks like useEffect are not executed, and objects like window or document are not available, causing errors.
      renderSSR.serverOnly(() => (
        <Component>
          <div>Test Content</div>
        </Component>
      ));
    
      expect(screen.getByText('Test Content')).toBeInTheDocument();
    });
    
    it('should render children correctly', async () => {
      // renderSSR is a method that renders the component in the client environment.
      // However, if the HTML rendered on the server and the HTML rendered on the client are different, hydration mismatch errors will occur.
      await renderSSR(() => (
        <Component>
          <div>Test Content</div>
        </Component>
      ));
    
      expect(screen.getByText('Test Content')).toBeInTheDocument();
    });
    
    it('should hydration mismatch error occurred', async () => {
      // This test code will fail due to a hydration mismatch error.
      await renderSSR(() => (
        <Component>
          <div>Test Content</div>
          <div>{Math.random()}</div>
        </Component>
      ));
    
      expect(screen.getByText('Test Content')).toBeInTheDocument();
    });
  • Hook Testing

    ts
    it('is safe on server side rendering', () => {
      // renderHookSSR.serverOnly is a method that renders the hook in the server environment.
      // In this environment, hooks like useEffect are not executed, and objects like window or document are not available, causing errors.
      const result = renderHookSSR.serverOnly(() => useToggle(true));
      const [bool] = result.current;
      expect(bool).toBe(true);
    });
    
    it('should initialize with the default value true', async () => {
      const { result } = await renderHookSSR(() => useToggle(true));
      const [bool] = result.current;
      expect(bool).toBe(true);
    });

Creating a Changeset

When your code changes affect the package, you need to create a changeset. Changesets are a tool that automates version management and changelog generation.

How to Create a Changeset

  1. After implementing your changes, run the following command:
bash
yarn changeset
  1. Select the type of change:

    • patch: Bug fixes or minor changes
    • minor: New features (maintaining backward compatibility)
    • major: Breaking changes (breaking backward compatibility)
  2. Write a brief summary of your changes.

TIP

The package is in the 0.x stage. During this phase, most changes should use patch. If you're unsure about the version type, please discuss with the maintainers.

  1. Commit the generated changeset file with your PR.

TIP

Changeset files are created in the .changeset folder and must be committed with your PR. When the PR is merged, the version will be automatically updated and a changelog will be generated.

Release

When changes are merged into the main branch, the release process happens automatically:

  1. When a PR is merged into the main branch, GitHub Actions will run.
  2. If there are changesets, a version update PR will be automatically created.
  3. When the version update PR is merged, the new version will be published to npm.

You can view the release results in GitHub Actions.

Documentation Contribution

There are no specific conditions for contributing to documentation. If you find incorrect information, poor translations, or have additional content to add, feel free to make edits. Please write documentation clearly and concisely from the reader's perspective.

Scaffolding

There's a command that creates the minimum skeleton for contributions. Use the following command to create an implementation folder with a basic structure:

bash
yarn run scaffold <name> --type <type>
  • type: Implementation type, must be one of component, hook, or util.
  • name: Name of the implementation.

Example

bash
yarn run scaffold Button --type component

This command creates three files in the src/components/Button folder:

tsx
/**
 * @description
 * <description-here>
 *
 * @param {<param-type>} <param-name> - <param-description>
 * @param {<param-type>} [<param-name>] - <optional-param-description>
 *
 * @returns {<return-type>} <return-description>
 * - <member-description> `<member-name>` - <member-description>
 *
 * @example
 * <example-code>
 */
export function Button() {
  // TODO: Implement Button
}
tsx
import { describe, expect, it } from 'vitest';

import { renderSSR } from '../../_internal/test-utils/renderSSR.tsx';

import { Button } from './Button.tsx';

describe('Button', () => {
  it('is safe on server side rendering', async () => {
    const result = renderSSR.serverOnly(() => <Button />);
    expect(true).toBe(true);
  });

  it('should work', async () => {
    const result = renderSSR.serverOnly(() => <Button />);
    expect(true).toBe(true);
  });
});
ts
export { Button } from './Button.tsx';

TIP

You can also use these shortcuts:

bash
yarn run scaffold Button --t c // Create component
yarn run scaffold useButton --t h // Create hook
yarn run scaffold getButton --t u // Create util

Contribution Workflow

Scaffold → Implementation → Testing → Documentation → Review → Changeset → Merge

Coverage Checklist

  • [ ] All if/else branches
  • [ ] All switch cases
  • [ ] All early returns
  • [ ] Cleanup functions (useEffect return)

Implementation Rules

  • Named exports only
  • Maximize TypeScript inference
  • Required parameters first, optional ones last; use an options object once there are three or more optional parameters
  • Return a single value when there is one, a [state, action] tuple for a pair, and an object when there are more members or when the shape is expected to grow (browser measurements such as keyboard height or safe-area insets)
  • Apply the SSR safety pattern below

SSR Safety Pattern

Never read a browser API while rendering — the server has no window, and a client value that differs from the server's causes a hydration mismatch. Start from a fixed value and synchronize in an effect:

ts
const [state, setState] = useState(FIXED_INITIAL_VALUE);

useEffect(function syncBrowserState() {
  if (isServer()) {
    return;
  }

  setState(readBrowserApi());
}, []);

Browser and Mobile Web Hooks

  • Throttle high-frequency events (scroll, resize, visualViewport changes) at about 16ms, skip updates when the value has not changed, and use startTransition for non-urgent updates

  • Use passive event listeners where the handler never calls preventDefault

  • Account for platform differences instead of picking one platform:

    FeatureiOSAndroid
    visualViewport.offsetTopBecomes negative when keyboard appearsTypically remains 0
    Keyboard behaviorViewport is pushed upResizes the layout
  • jsdom cannot reproduce the visual viewport or the on-screen keyboard, so verify these hooks on a real iOS Safari and Android Chrome device as well as in tests

Released under the MIT License.