Skip to content
This repository has been archived by the owner on Jul 8, 2023. It is now read-only.

Commit

Permalink
➕ add with-callback-once
Browse files Browse the repository at this point in the history
  • Loading branch information
deepsweet committed Aug 21, 2017
1 parent ead00c8 commit 72e7460
Show file tree
Hide file tree
Showing 7 changed files with 301 additions and 0 deletions.
29 changes: 29 additions & 0 deletions packages/with-callback-once/demo/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/* eslint-disable no-console */
import React from 'react';
import { compose, withState, withHandlers } from 'recompose';
import withCallbackOnChangeWhile from '../../with-callback-on-change-while/src';

import withCallbackOnce from '../src';

const Demo = ({ count, onButtonClick }) => (
<div>
<h1>{count}</h1>
<button onClick={onButtonClick}>decrement</button>
</div>
);

export default compose(
withState('count', 'setCount', 5),
withHandlers({
onButtonClick: ({ setCount, count }) => () => setCount(count - 1)
}),
withCallbackOnChangeWhile(
'count',
({ count }) => count >= 0,
({ count }) => console.log(count)
),
withCallbackOnce(
({ count }) => count === 0,
() => console.log('done!')
)
)(Demo);
32 changes: 32 additions & 0 deletions packages/with-callback-once/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "@hocs/with-callback-once",
"library": "withCallbackOnce",
"version": "0.1.0",
"description": "Invokes a callback once condition is true as a HOC for React",
"keywords": [
"react",
"hoc",
"recompose",
"callback"
],
"main": "lib/index.js",
"module": "es/index.js",
"files": [
"dist/",
"es/",
"lib/"
],
"repository": "deepsweet/hocs",
"author": "Kir Belevich <kir@belevi.ch> (https://github.com/deepsweet)",
"license": {
"type": "MIT",
"url": "https://github.com/deepsweet/hocs/blob/master/license.md"
},
"publishConfig": {
"access": "public"
},
"peerDependencies": {
"react": "^15.6.1",
"recompose": "^0.25.0"
}
}
54 changes: 54 additions & 0 deletions packages/with-callback-once/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# :bell: with-callback-once

[![npm](https://img.shields.io/npm/v/@hocs/with-callback-once.svg?style=flat-square)](https://www.npmjs.com/package/@hocs/with-callback-once) [![ci](https://img.shields.io/travis/deepsweet/hocs/master.svg?style=flat-square)](https://travis-ci.org/deepsweet/hocs) [![coverage](https://img.shields.io/codecov/c/github/deepsweet/hocs/master.svg?style=flat-square)](https://codecov.io/github/deepsweet/hocs) [![deps](https://david-dm.org/deepsweet/hocs.svg?path=packages/with-callback-once&style=flat-square)](https://david-dm.org/deepsweet/hocs?path=packages/with-callback-once)

Part of a [collection](https://github.com/deepsweet/hocs) of Higher-Order Components for React, especially useful with [Recompose](https://github.com/acdlite/recompose).

Invokes a callback once condition is true (while previous check should was false), useful to decouple side effects like `onSuccess` or `onError` handlers in a declarative way.

## Install

```
yarn add recompose @hocs/with-callback-once
```

## Usage

```js
withCallbackOnce(
shouldCall: (props: Object) => boolean,
callback: (props: Object) => void
): HigherOrderComponent
```

```js
import React from 'react';
import { compose, withState, withHandlers } from 'recompose';
import withCallbackOnChangeWhile from '@hocs/with-callback-on-change-while';
import withCallbackOnce from '@hocs/with-callback-once';

const Demo = ({ count, onButtonClick }) => (
<div>
<h1>{count}</h1>
<button onClick={onButtonClick}>decrement</button>
</div>
);

export default compose(
withState('count', 'setCount', 5),
withHandlers({
onButtonClick: ({ setCount, count }) => () => setCount(count - 1)
}),
withCallbackOnChangeWhile(
'count',
({ count }) => count >= 0,
({ count }) => console.log(count)
),
withCallbackOnce(
({ count }) => count === 0,
() => console.log('done!')
)
)(Demo);
```

:tv: [Check out live demo](https://www.webpackbin.com/bins/-Ks5CKJe7OAudCaByQzw).
29 changes: 29 additions & 0 deletions packages/with-callback-once/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Component } from 'react';
import { createEagerFactory, setDisplayName, wrapDisplayName } from 'recompose';

const withCallbackOnce = (shouldCall, callback) => (Target) => {
const factory = createEagerFactory(Target);

class WithCallbackOnce extends Component {
componentWillReceiveProps(nextProps) {
if (
shouldCall(this.props) === false &&
shouldCall(nextProps) === true
) {
callback(nextProps);
}
}

render() {
return factory(this.props);
}
}

if (process.env.NODE_ENV !== 'production') {
return setDisplayName(wrapDisplayName(Target, 'withCallbackOnce'))(WithCallbackOnce);
}

return WithCallbackOnce;
};

export default withCallbackOnce;
76 changes: 76 additions & 0 deletions packages/with-callback-once/test/__snapshots__/index.jsx.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`withCallbackOnce display name should not wrap display name in production env 1`] = `<WithCallbackOnce />`;

exports[`withCallbackOnce display name should wrap display name in non-production env 1`] = `
<withCallbackOnce(Target)>
<Target />
</withCallbackOnce(Target)>
`;

exports[`withCallbackOnce should invoke a callback when condition status has been changed 1`] = `Array []`;

exports[`withCallbackOnce should invoke a callback when condition status has been changed 2`] = `Array []`;

exports[`withCallbackOnce should invoke a callback when condition status has been changed 3`] = `
Array [
Array [
Object {
"a": 1,
"b": 2,
"c": 3,
},
],
Array [
Object {
"a": 11,
"b": 2,
"c": 3,
},
],
]
`;

exports[`withCallbackOnce should invoke a callback when condition status has been changed 4`] = `
Array [
Array [
Object {
"a": 11,
"b": 2,
"c": 3,
},
],
]
`;

exports[`withCallbackOnce should no-op if condition status is the same 1`] = `Array []`;

exports[`withCallbackOnce should no-op if condition status is the same 2`] = `Array []`;

exports[`withCallbackOnce should no-op if condition status is the same 3`] = `
Array [
Array [
Object {
"a": 1,
"b": 2,
"c": 3,
},
],
]
`;

exports[`withCallbackOnce should no-op if condition status is the same 4`] = `Array []`;

exports[`withCallbackOnce should pass props through 1`] = `
<withCallbackOnce(Target)
a={1}
b={2}
c={3}
>
<Target
a={1}
b={2}
c={3}
/>
</withCallbackOnce(Target)>
`;
77 changes: 77 additions & 0 deletions packages/with-callback-once/test/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import React from 'react';
import { mount } from 'enzyme';

import withCallbackOnce from '../src/';

const Target = () => null;

describe('withCallbackOnce', () => {
it('should pass props through', () => {
const EnhancedTarget = withCallbackOnce()(Target);
const wrapper = mount(
<EnhancedTarget a={1} b={2} c={3}/>
);

expect(wrapper).toMatchSnapshot();
});

it('should invoke a callback when condition status has been changed', () => {
const mockShouldCall = jest.fn(() => true).mockImplementationOnce(() => false);
const mockCallback = jest.fn();
const EnhancedTarget = withCallbackOnce(mockShouldCall, mockCallback)(Target);
const wrapper = mount(
<EnhancedTarget a={1} b={2} c={3}/>
);

expect(mockShouldCall.mock.calls).toMatchSnapshot();
expect(mockCallback.mock.calls).toMatchSnapshot();
wrapper.setProps({ a: 11 });
expect(mockShouldCall.mock.calls).toMatchSnapshot();
expect(mockCallback.mock.calls).toMatchSnapshot();
});

it('should no-op if condition status is the same', () => {
const mockShouldCall = jest.fn(() => true);
const mockCallback = jest.fn();
const EnhancedTarget = withCallbackOnce(mockShouldCall, mockCallback)(Target);
const wrapper = mount(
<EnhancedTarget a={1} b={2} c={3}/>
);

expect(mockShouldCall.mock.calls).toMatchSnapshot();
expect(mockCallback.mock.calls).toMatchSnapshot();
wrapper.setProps({ a: 11 });
expect(mockShouldCall.mock.calls).toMatchSnapshot();
expect(mockCallback.mock.calls).toMatchSnapshot();
});

describe('display name', () => {
const origNodeEnv = process.env.NODE_ENV;

afterAll(() => {
process.env.NODE_ENV = origNodeEnv;
});

it('should wrap display name in non-production env', () => {
process.env.NODE_ENV = 'test';

const EnhancedTarget = withCallbackOnce()(Target);
const wrapper = mount(
<EnhancedTarget/>
);

expect(wrapper).toMatchSnapshot();
});

it('should not wrap display name in production env', () => {
process.env.NODE_ENV = 'production';

const EnhancedTarget = withCallbackOnce()(Target);
const wrapper = mount(
<EnhancedTarget/>
);

expect(wrapper).toMatchSnapshot();
});
});
});
4 changes: 4 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ Invokes a callback on prop change, useful to decouple side effects in a declarat

Invokes a callback on prop change while condition is true, useful to decouple side effects in a declarative way.

### :bell: [with-callback-once](packages/with-callback-once)

Invokes a callback once condition is true (while previous check should was false), useful to decouple side effects like `onSuccess` or `onError` handlers in a declarative way.

### :mag: [with-log](packages/with-log)

Injects `console.log` with props or any custom message into render.
Expand Down

0 comments on commit 72e7460

Please sign in to comment.