Skip to content

Commit

Permalink
feat(react-hooks): adding use controlled state hook (#201)
Browse files Browse the repository at this point in the history
  • Loading branch information
bclark-p44 authored Jan 25, 2023
1 parent 282de5d commit 6ce808f
Show file tree
Hide file tree
Showing 13 changed files with 247 additions and 3 deletions.
5 changes: 5 additions & 0 deletions .changeset/chatty-mugs-vanish.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@project44-manifest/use-controlled-state': minor
---

Adding useControlledState hook package
18 changes: 18 additions & 0 deletions packages/use-controlled-state/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
MIT License

Copyright (c) 2021 project44, Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute,
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
4 changes: 4 additions & 0 deletions packages/use-controlled-state/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# @project44-react/react-use-controlled-state

This package is part of [Manifest Design System](https://github.com/project44/manifest). Please see
the repo for more details.
4 changes: 4 additions & 0 deletions packages/use-controlled-state/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = {
preset: 'jest-preset-manifest',
testEnvironment: 'jest-environment-jsdom',
};
7 changes: 7 additions & 0 deletions packages/use-controlled-state/moon.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
type: 'library'

tasks:
build:
outputs:
- 'esm'
- 'lib'
55 changes: 55 additions & 0 deletions packages/use-controlled-state/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"name": "@project44-manifest/use-controlled-state",
"version": "0.0.0",
"description": "Manifest Design System react hook for controlled state",
"license": "MIT",
"author": "project44",
"keywords": [
"manifest",
"design",
"system",
"react",
"hooks"
],
"sideEffects": false,
"main": "./lib/index.js",
"module": "./esm/index.js",
"types": "./dts/index.d.ts",
"files": [
"dts/**/*.d.ts",
"esm/**/*.{js,map}",
"lib/**/*.{js,map}",
"src/**/*.{ts,tsx,json}"
],
"repository": {
"type": "git",
"url": "git@github.com:project-44/manifest.git",
"directory": "packages/use-controlled-state"
},
"publishConfig": {
"access": "public"
},
"peerDependencies": {
"react": "^17.0.0 || ^18.0.0",
"react-dom": "^17.0.0 || ^18.0.0"
},
"devDependencies": {
"react": "^18.1.0",
"react-dom": "^18.1.0"
},
"packemon": {
"platform": "browser"
},
"exports": {
"./package.json": "./package.json",
".": {
"types": "./dts/index.d.ts",
"browser": {
"module": "./esm/index.js",
"import": "./esm/index.js",
"default": "./lib/index.js"
},
"default": "./lib/index.js"
}
}
}
1 change: 1 addition & 0 deletions packages/use-controlled-state/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useControlledState';
45 changes: 45 additions & 0 deletions packages/use-controlled-state/src/useControlledState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import * as React from 'react';

/**
* A react hook used to manage controlled and uncontrolled states.
*/
export function useControlledState<T>(
value: T,
defaultValue: T,
onChange: (value: T, ...args: any[]) => void,
): [T, (value: T, ...args: any[]) => void] {
const [stateValue, setStateValue] = React.useState(defaultValue);

const { current: isControlled } = React.useRef(value !== undefined);
const prevValueRef = React.useRef(stateValue);

const handleChange = React.useCallback(
(nextValue: T, ...args: any[]) => {
if (prevValueRef.current !== nextValue) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
onChange?.(nextValue, ...args);
}

if (!isControlled) {
prevValueRef.current = nextValue;
}
},
[isControlled, onChange, prevValueRef],
);

const handleSetValue = React.useCallback(
(nextValue: T, ...args: any[]) => {
if (!isControlled) setStateValue(nextValue);

// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
handleChange(nextValue, ...args);
},
[handleChange, isControlled],
);

if (isControlled) {
prevValueRef.current = value;
}

return [isControlled ? value : stateValue, handleSetValue];
}
64 changes: 64 additions & 0 deletions packages/use-controlled-state/tests/useControlledState.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { act, renderHook } from '@testing-library/react';
import { useControlledState } from '../src';

describe('useControlledState', () => {
it('should handle uncontrolled values', () => {
const onChange = jest.fn();

const { result } = renderHook(() =>
useControlledState<string | undefined>(undefined, 'defaultValue', onChange),
);

let [value, setValue] = result.current;

expect(value).toBe('defaultValue');
expect(onChange).not.toHaveBeenCalled();

act(() => void setValue('newValue'));

[value, setValue] = result.current;

expect(value).toBe('newValue');
expect(onChange).toHaveBeenLastCalledWith('newValue');

onChange.mockClear();

// Should not call onChange if value is the same.
act(() => void setValue('newValue'));

[value, setValue] = result.current;

expect(value).toBe('newValue');
expect(onChange).not.toHaveBeenCalled();
});

it('should handle controlled values', () => {
const onChange = jest.fn();

const { result } = renderHook(() =>
useControlledState<string>('value', 'defaultValue', onChange),
);

let [value, setValue] = result.current;

expect(value).toBe('value');
expect(onChange).not.toHaveBeenCalled();

act(() => void setValue('newValue'));

[value, setValue] = result.current;

expect(value).toBe('value');
expect(onChange).toHaveBeenLastCalledWith('newValue');

onChange.mockClear();

act(() => void setValue('value'));

[value, setValue] = result.current;

// Should not call onChange if value is the same.
expect(value).toBe('value');
expect(onChange).not.toHaveBeenCalled();
});
});
11 changes: 11 additions & 0 deletions packages/use-controlled-state/tsconfig.build.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.options.json",
"compilerOptions": {
"outDir": "dts",
"rootDir": "src"
},
"include": [
"src/**/*"
],
"references": []
}
15 changes: 15 additions & 0 deletions packages/use-controlled-state/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"extends": "../../tsconfig.options.json",
"compilerOptions": {
"outDir": "../../.moon/cache/types/packages/use-controlled-state"
},
"include": [
"src/**/*",
"tests/**/*"
],
"references": [
{
"path": "./tsconfig.build.json"
}
]
}
9 changes: 6 additions & 3 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
"extends": "./tsconfig.options.json",
"files": [],
"references": [
{
"path": "packages/design-tokens"
},
{
"path": "packages/button"
},
{
"path": "packages/css-baseline"
},
{
"path": "packages/design-tokens"
},
{
"path": "packages/layout"
},
Expand Down Expand Up @@ -41,6 +41,9 @@
{
"path": "packages/typography"
},
{
"path": "packages/use-controlled-state"
},
{
"path": "packages/utils"
},
Expand Down
12 changes: 12 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3495,6 +3495,18 @@ __metadata:
languageName: unknown
linkType: soft

"@project44-manifest/use-controlled-state@workspace:packages/use-controlled-state":
version: 0.0.0-use.local
resolution: "@project44-manifest/use-controlled-state@workspace:packages/use-controlled-state"
dependencies:
react: ^18.1.0
react-dom: ^18.1.0
peerDependencies:
react: ^17.0.0 || ^18.0.0
react-dom: ^17.0.0 || ^18.0.0
languageName: unknown
linkType: soft

"@project44-manifest/utils@npm:^1.0.1":
version: 1.0.1
resolution: "@project44-manifest/utils@npm:1.0.1"
Expand Down

1 comment on commit 6ce808f

@vercel
Copy link

@vercel vercel bot commented on 6ce808f Jan 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.