Skip to content
This repository has been archived by the owner on Jan 10, 2025. It is now read-only.

Commit

Permalink
Add useToggle hook to react-hooks (#1220)
Browse files Browse the repository at this point in the history
  • Loading branch information
BPScott authored Dec 13, 2019
1 parent eee721d commit ef0ff1a
Show file tree
Hide file tree
Showing 11 changed files with 115 additions and 9 deletions.
2 changes: 2 additions & 0 deletions packages/react-hooks/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ and adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

<!-- ## [Unreleased] -->

- Added `useToggle` hook ([#1220](https://github.com/Shopify/quilt/pull/1220))

## [1.3.0] - 2019-10-29

- Added a `usePrevious` hook ([#1145](https://github.com/Shopify/quilt/pull/1145))
Expand Down
25 changes: 25 additions & 0 deletions packages/react-hooks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,28 @@ function Score({value}) {
);
}
```

### `useToggle()`

This hook will provide an object that contains a boolean state value and a set of memoised callbacks to toggle it, force it to true and force it to false. It accepts one argument that is the initial value of the state. This is useful for toggling the active state of modals and popovers.

```tsx
function MyComponent() {
const {
value: isActive,
toggle: toggleIsActive,
setTrue: setIsActiveTrue,
setFalse: setIsActiveFalse,
} = useToggle(false);
const activeText = isActive ? 'true' : 'false';

return (
<>
<p>Value: {activeText}</p>
<button onClick={toggleIsActive}>Toggle isActive state</button>
<button onClick={setIsActiveTrue}>Set isActive state to true</button>
<button onClick={setIsActiveFalse}>Set isActive state to false</button>
</>
);
}
```
7 changes: 4 additions & 3 deletions packages/react-hooks/src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export {useLazyRef} from './lazy-ref';
export {default as useTimeout} from './timeout';
export {default as useOnValueChange} from './on-value-change';
export {useMountedRef} from './mounted-ref';
export {default as usePrevious} from './previous';
export {useOnValueChange} from './on-value-change';
export {usePrevious} from './previous';
export {useTimeout} from './timeout';
export {useToggle} from './toggle';
2 changes: 1 addition & 1 deletion packages/react-hooks/src/hooks/on-value-change.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';

export default function useOnValueChange<T>(
export function useOnValueChange<T>(
value: T,
onChange: (value: T, oldValue: T) => void,
) {
Expand Down
2 changes: 1 addition & 1 deletion packages/react-hooks/src/hooks/previous.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {useRef, useEffect} from 'react';

export default function usePrevious<T>(value: T) {
export function usePrevious<T>(value: T) {
const ref = useRef<T>();
useEffect(() => {
ref.current = value;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import {mount} from '@shopify/react-testing';

import useOnValueChange from '../on-value-change';
import {useOnValueChange} from '../on-value-change';

describe('useOnValueChnge', () => {
function MockComponent({value, spy}) {
Expand Down
2 changes: 1 addition & 1 deletion packages/react-hooks/src/hooks/tests/previous.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as React from 'react';
import {mount} from '@shopify/react-testing';

import usePrevious from '../previous';
import {usePrevious} from '../previous';

describe('usePrevious', () => {
function Score({value}) {
Expand Down
2 changes: 1 addition & 1 deletion packages/react-hooks/src/hooks/tests/timeout.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import {mount} from '@shopify/react-testing';
import {timer} from '@shopify/jest-dom-mocks';

import useTimeout from '../timeout';
import {useTimeout} from '../timeout';

describe('useTimeout', () => {
function MockComponent({spy, delay}) {
Expand Down
62 changes: 62 additions & 0 deletions packages/react-hooks/src/hooks/tests/toggle.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import React from 'react';
import {mount} from '@shopify/react-testing';

import {useToggle} from '../toggle';

describe('useToggle', () => {
function MockComponent({initialValueIsTrue = false}) {
const {value, toggle, setTrue, setFalse} = useToggle(initialValueIsTrue);

const activeText = value ? 'true' : 'false';

return (
<>
<p>Value: {activeText}</p>
<button type="button" id="toggle" onClick={toggle} />
<button type="button" id="forceTrue" onClick={setTrue} />
<button type="button" id="forceFalse" onClick={setFalse} />
</>
);
}

it('starts with an initial value', () => {
const wrapperInitallyFalse = mount(<MockComponent />);
expect(wrapperInitallyFalse).toContainReactText('Value: false');

const wrapperInitallyTrue = mount(<MockComponent initialValueIsTrue />);
expect(wrapperInitallyTrue).toContainReactText('Value: true');
});

it('toggles the value when the toggle callback is triggered', () => {
const wrapper = mount(<MockComponent />);
expect(wrapper).toContainReactText('Value: false');

wrapper.find('button', {id: 'toggle'}).trigger('onClick');
expect(wrapper).toContainReactText('Value: true');

wrapper.find('button', {id: 'toggle'}).trigger('onClick');
expect(wrapper).toContainReactText('Value: false');
});

it('forces the value to true when the forceTrue callback is triggered', () => {
const wrapper = mount(<MockComponent />);
expect(wrapper).toContainReactText('Value: false');

wrapper.find('button', {id: 'forceTrue'}).trigger('onClick');
expect(wrapper).toContainReactText('Value: true');

wrapper.find('button', {id: 'forceTrue'}).trigger('onClick');
expect(wrapper).toContainReactText('Value: true');
});

it('forces the value to false when the forceFalse callback is triggered', () => {
const wrapper = mount(<MockComponent initialValueIsTrue />);
expect(wrapper).toContainReactText('Value: true');

wrapper.find('button', {id: 'forceFalse'}).trigger('onClick');
expect(wrapper).toContainReactText('Value: false');

wrapper.find('button', {id: 'forceFalse'}).trigger('onClick');
expect(wrapper).toContainReactText('Value: false');
});
});
2 changes: 1 addition & 1 deletion packages/react-hooks/src/hooks/timeout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';

export default function useTimeout(callback: () => void, delay: number) {
export function useTimeout(callback: () => void, delay: number) {
React.useEffect(() => {
const id = setTimeout(callback, delay);
return () => clearTimeout(id);
Expand Down
16 changes: 16 additions & 0 deletions packages/react-hooks/src/hooks/toggle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {useState, useCallback} from 'react';

/**
* Returns a stateful value, and a set of memoized functions to toggle it,
* set it to true and set it to false
*/
export function useToggle(initialState: boolean) {
const [value, setState] = useState(initialState);

return {
value,
toggle: useCallback(() => setState(state => !state), []),
setTrue: useCallback(() => setState(true), []),
setFalse: useCallback(() => setState(false), []),
};
}

0 comments on commit ef0ff1a

Please sign in to comment.