Skip to content

Commit 90a8874

Browse files
committed
Update function argument API slightly, allow for single return values
1 parent a3f2387 commit 90a8874

File tree

3 files changed

+90
-78
lines changed

3 files changed

+90
-78
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ const useForm = (initialValue = '') => {
111111
}
112112

113113
const FormContainer = composeHooks(props => ({
114-
useForm: useForm(props.initialValue)
114+
useForm: () => useForm(props.initialValue)
115115
})(FormPresenter);
116116

117117
<FormContainer initialValue="Susie" />

src/__tests__/index.test.js

Lines changed: 74 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
/* eslint-disable react/prop-types */
22
/* eslint-disable react/button-has-type */
3-
import React, { useState } from "react";
4-
import { shallow, mount } from "enzyme";
5-
import composeHooks from "../index";
3+
import React, { useState } from 'react';
4+
import { shallow, mount } from 'enzyme';
5+
import composeHooks from '../index';
66

77
const INITIAL_COUNT = 0;
8-
const INITIAL_VALUE = "hi";
8+
const INITIAL_VALUE = 'hi';
99

1010
const useCount = () => {
1111
const [count, setCount] = useState(INITIAL_COUNT);
@@ -24,70 +24,34 @@ const useUseState = () => useState(INITIAL_COUNT);
2424

2525
const TestComponent = () => <div>Test</div>;
2626

27-
test("returns component if no hooks", () => {
28-
const Container = composeHooks()(TestComponent);
29-
const wrapper = shallow(<Container />);
30-
expect(wrapper.html()).toMatchInlineSnapshot(`"<div>Test</div>"`);
31-
});
32-
33-
test("throws if no component", () => {
34-
expect(() => composeHooks()()).toThrowErrorMatchingInlineSnapshot(
35-
`"Component must be provided to compose"`
36-
);
37-
});
38-
39-
test("passes custom hooks to component", () => {
27+
test('passes custom hooks to component', () => {
4028
const Container = composeHooks({ useCount, useChange })(TestComponent);
4129
const wrapper = shallow(<Container />);
42-
const { count, increment, decrement, value, onChange } = wrapper
43-
.find(TestComponent)
44-
.props();
30+
const { count, increment, decrement, value, onChange } = wrapper.find(TestComponent).props();
4531
expect(count).toBe(INITIAL_COUNT);
4632
expect(value).toBe(INITIAL_VALUE);
47-
expect(typeof increment).toBe("function");
48-
expect(typeof decrement).toBe("function");
49-
expect(typeof onChange).toBe("function");
33+
expect(typeof increment).toBe('function');
34+
expect(typeof decrement).toBe('function');
35+
expect(typeof onChange).toBe('function');
5036
});
5137

52-
test("passes props to component", () => {
38+
test('passes props to component', () => {
5339
const Container = composeHooks({ useChange })(TestComponent);
5440
const wrapper = shallow(<Container foo="bar" />);
5541
const { foo } = wrapper.find(TestComponent).props();
56-
expect(foo).toBe("bar");
57-
});
58-
59-
test("if prop and hook names collide, props win", () => {
60-
const Container = composeHooks({ useChange })(TestComponent);
61-
const wrapper = shallow(<Container />);
62-
expect(wrapper.find(TestComponent).props().value).toBe("hi");
63-
wrapper.setProps({ value: "newValue" });
64-
expect(wrapper.find(TestComponent).props().value).toBe("newValue");
65-
});
66-
67-
test("warns on hook name collisions", () => {
68-
console.warn = jest.fn().mockImplementationOnce(() => {});
69-
const useChangeTwo = () => ({ value: "duplicate-hook-prop" });
70-
const Container = composeHooks({ useChange, useChangeTwo })(TestComponent);
71-
const wrapper = shallow(<Container />);
72-
expect(console.warn.mock.calls[0][0]).toMatchInlineSnapshot(
73-
`"prop 'value' exists, overriding with value: duplicate-hook-prop"`
74-
);
75-
expect(wrapper.find(TestComponent).props().value).toBe("duplicate-hook-prop");
76-
jest.restoreAllMocks();
42+
expect(foo).toBe('bar');
7743
});
7844

79-
test("hooks work as expected", () => {
80-
const Component = ({ value, onChange }) => (
81-
<input value={value} onChange={onChange} />
82-
);
45+
test('hooks work as expected', () => {
46+
const Component = ({ value, onChange }) => <input value={value} onChange={onChange} />;
8347
const Container = composeHooks({ useChange })(Component);
8448
const wrapper = mount(<Container />);
85-
expect(wrapper.find("input").props().value).toBe(INITIAL_VALUE);
86-
wrapper.find("input").simulate("change", { target: { value: "new" } });
87-
expect(wrapper.find("input").props().value).toBe("new");
49+
expect(wrapper.find('input').props().value).toBe(INITIAL_VALUE);
50+
wrapper.find('input').simulate('change', { target: { value: 'new' } });
51+
expect(wrapper.find('input').props().value).toBe('new');
8852
});
8953

90-
test("works with custom hook that returns array", () => {
54+
test('works with custom hook that returns array', () => {
9155
const Component = ({ simpleHook }) => {
9256
const [count, setCount] = simpleHook;
9357
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
@@ -96,16 +60,71 @@ test("works with custom hook that returns array", () => {
9660
const Container = composeHooks({ simpleHook: useUseState })(Component);
9761
const wrapper = mount(<Container />);
9862
expect(wrapper.text()).toBe(INITIAL_COUNT.toString());
99-
wrapper.find("button").simulate("click");
63+
wrapper.find('button').simulate('click');
10064
expect(wrapper.text()).toBe((INITIAL_COUNT + 1).toString());
10165
});
10266

67+
test('works with custom hook that returns single value', () => {
68+
// Check single function value
69+
let outerFoo;
70+
const useFoo = () => {
71+
const [foo, setFoo] = useState('before');
72+
outerFoo = foo;
73+
return setFoo;
74+
};
75+
// Check single value
76+
const useBar = () => {
77+
const [bar] = useState('Click me');
78+
return bar;
79+
};
80+
const Component = ({ setFoo, bar }) => <button onClick={() => setFoo('after')}>{bar}</button>;
81+
const Container = composeHooks({ setFoo: useFoo, bar: useBar })(Component);
82+
const wrapper = mount(<Container />);
83+
expect(outerFoo).toBe('before');
84+
wrapper.find({ children: 'Click me' }).simulate('click');
85+
expect(outerFoo).toBe('after');
86+
});
87+
10388
test('can pass props to hooks via function', () => {
10489
const TEST_VALUE = 'test-value';
10590
const Component = ({ value }) => value;
10691
const Container = composeHooks(props => ({
107-
useChange: useChange(props.initialValue)
92+
useChange: () => useChange(props.initialValue),
10893
}))(Component);
10994
const wrapper = mount(<Container initialValue={TEST_VALUE} />);
11095
expect(wrapper.text()).toBe(TEST_VALUE);
11196
});
97+
98+
describe('Edge cases', () => {
99+
it('returns component if no hooks', () => {
100+
const Container = composeHooks()(TestComponent);
101+
const wrapper = shallow(<Container />);
102+
expect(wrapper.html()).toMatchInlineSnapshot(`"<div>Test</div>"`);
103+
});
104+
105+
it('throws if no component', () => {
106+
expect(() => composeHooks()()).toThrowErrorMatchingInlineSnapshot(
107+
`"Component must be provided to compose"`
108+
);
109+
});
110+
111+
it('if prop and hook names collide, props win', () => {
112+
const Container = composeHooks({ useChange })(TestComponent);
113+
const wrapper = shallow(<Container />);
114+
expect(wrapper.find(TestComponent).props().value).toBe('hi');
115+
wrapper.setProps({ value: 'newValue' });
116+
expect(wrapper.find(TestComponent).props().value).toBe('newValue');
117+
});
118+
119+
it('warns on hook name collisions', () => {
120+
console.warn = jest.fn().mockImplementationOnce(() => {});
121+
const useChangeTwo = () => ({ value: 'duplicate-hook-prop' });
122+
const Container = composeHooks({ useChange, useChangeTwo })(TestComponent);
123+
const wrapper = shallow(<Container />);
124+
expect(console.warn.mock.calls[0][0]).toMatchInlineSnapshot(
125+
`"prop 'value' exists, overriding with value: duplicate-hook-prop"`
126+
);
127+
expect(wrapper.find(TestComponent).props().value).toBe('duplicate-hook-prop');
128+
jest.restoreAllMocks();
129+
});
130+
});

src/index.js

Lines changed: 15 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,35 +10,28 @@ const composeHooks = hooks => Component => {
1010
}
1111

1212
return props => {
13-
// TODO: Potentially do some optimization similar to what react-redux
14-
// does for mapStateToProps:
13+
// TODO: Potentially optimize similar to mapStateToProps in react-redux
1514
// https://github.com/reduxjs/react-redux/blob/master/src/connect/wrapMapToProps.js
1615

17-
const hooksIsFunc = typeof hooks === 'function';
16+
const hooksObject = typeof hooks === 'function' ? hooks(props) : hooks;
1817

19-
const hooksObject = hooksIsFunc ? hooks(props) : hooks;
18+
// Flatten values from all hooks to a single object
19+
const hooksProps = Object.entries(hooksObject).reduce((acc, [hookKey, hook]) => {
20+
let hookValue = hook();
2021

21-
const hooksProps = Object.entries(hooksObject).reduce(
22-
(acc, [hookKey, hookValue]) => {
23-
const hookReturnValue = hooksIsFunc ? hookValue : hookValue();
22+
if (Array.isArray(hookValue) || typeof hookValue !== 'object') {
23+
hookValue = { [hookKey]: hookValue };
24+
}
2425

25-
if (Array.isArray(hookReturnValue)) {
26-
acc[hookKey] = hookReturnValue;
27-
return acc;
26+
Object.entries(hookValue).forEach(([key, value]) => {
27+
if (acc[key]) {
28+
console.warn(`prop '${key}' exists, overriding with value: ${value}`);
2829
}
30+
acc[key] = value;
31+
});
2932

30-
Object.entries(hookReturnValue).forEach(([key, value]) => {
31-
if (acc[key]) {
32-
console.warn(
33-
`prop '${key}' exists, overriding with value: ${value}`
34-
);
35-
}
36-
acc[key] = value;
37-
});
38-
return acc;
39-
},
40-
{}
41-
);
33+
return acc;
34+
}, {});
4235

4336
return <Component {...hooksProps} {...props} />;
4437
};

0 commit comments

Comments
 (0)