-
Notifications
You must be signed in to change notification settings - Fork 228
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: draft implementation of useStore in #733 (not production ready) #734
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import useFluxible from './useFluxible' | ||
|
||
/** | ||
* React hook that returns an executeAction handler | ||
* TODO: this is a draft for an ongoing discussion in #733 | ||
* | ||
* Example: | ||
* | ||
* const FooComponent = () => { | ||
* const executeAction = useExecuteAction(); | ||
* return <p id={foo} onClick={() => excuteAction(...)} />; | ||
* }; | ||
* | ||
* @function useExecuteAction | ||
* @returns {Function} - executeAction handler | ||
*/ | ||
const useExecuteAction = () => { | ||
const { executeAction } = useFluxible(); | ||
return executeAction; | ||
} | ||
|
||
export default useExecuteAction; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import {useEffect, useState, useLayoutEffect} from 'react'; | ||
import useFluxible from './useFluxible' | ||
|
||
/** | ||
* React hook that returns a state from Fluxible store. | ||
* TODO: this is a draft for an ongoing discussion in #733 | ||
* | ||
* Example: | ||
* | ||
* const FooComponent = () => { | ||
* const foo = useStore('FooStore', store => store.getFoo()); | ||
* return <p id={foo} />; | ||
* }; | ||
* | ||
* @function useStore | ||
* @returns {object} - a state from Fluxible store | ||
*/ | ||
const useStore = (storeName, getStateFromStore) => { | ||
const { getStore } = useFluxible(); | ||
const store = getStore(storeName); | ||
const [state, setState] = useState(getStateFromStore(store)); | ||
|
||
function updateState() { | ||
setState(getStateFromStore(store)); | ||
} | ||
|
||
// useLayoutEffect is the closest to componentDidMount | ||
// (we want to block render until store is subscribed) | ||
// TODO: NOTE useLayoutEffect is called on server-side during SSR | ||
useLayoutEffect(() => { | ||
store.on('change', updateState); | ||
return () => store.removeListener('change', updateState); | ||
}, []); | ||
|
||
return state; | ||
} | ||
|
||
export default useStore; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
import React, { useState } from 'react'; | ||
import { expect } from 'chai'; | ||
import TestRenderer from 'react-test-renderer'; | ||
import createMockComponentContext from 'fluxible/utils/createMockComponentContext'; | ||
import { useStore, FluxibleProvider, useExecuteAction, FluxibleComponent } from '../../../'; | ||
import FooStore from '../../fixtures/stores/FooStore'; | ||
import BarStore from '../../fixtures/stores/BarStore'; | ||
|
||
const DumbComponent = () => { | ||
|
||
const foo = useStore(FooStore, store => store.getFoo()); | ||
const bar = useStore(BarStore, store => store.getBar()); | ||
const executeAction = useExecuteAction(); | ||
const onClick = () => executeAction((context) => context.dispatch('DOUBLE_UP')) | ||
|
||
return ( | ||
<div> | ||
<span id="foo">{foo}</span> | ||
<span id="bar">{bar}</span> | ||
<button id="button" onClick={onClick} /> | ||
</div> | ||
) | ||
}; | ||
|
||
DumbComponent.displayName = 'DumbComponent'; | ||
DumbComponent.initAction = () => {}; | ||
|
||
const stores = [FooStore, BarStore]; | ||
|
||
const renderComponent = (Component) => { | ||
const context = createMockComponentContext({ stores }); | ||
|
||
const app = TestRenderer.create( | ||
<FluxibleComponent context={context}> | ||
<Component ref={undefined} /> | ||
</FluxibleComponent> | ||
); | ||
|
||
return { app, context }; | ||
}; | ||
|
||
describe('fluxible-addons-react', () => { | ||
describe('useStore', () => { | ||
it('returns fluxible store', () => { | ||
const FooComponent = () => { | ||
const foo = useStore('FooStore', store => store.getFoo()) | ||
return <p id={foo} />; | ||
}; | ||
|
||
const context = createMockComponentContext({ stores: [FooStore] }); | ||
|
||
const testRenderer = TestRenderer.create( | ||
<FluxibleProvider context={context}> | ||
<FooComponent /> | ||
</FluxibleProvider> | ||
); | ||
|
||
const component = testRenderer.root.findByType('p'); | ||
|
||
expect(component.props.id).to.deep.equal('bar'); | ||
}); | ||
it('should register/unregister from stores on mount/unmount', () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just to make it simple to navigate between code blocks, I would recommend you to add one blank line between each test. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep, I can do that. The IDE handles most of the reformatting though, so I might approach it by adding a lint rule. |
||
const { app, context } = renderComponent(DumbComponent); | ||
|
||
const barStore = context.getStore(BarStore); | ||
const fooStore = context.getStore(FooStore); | ||
|
||
expect(barStore.listeners('change').length).to.equal(1); | ||
expect(fooStore.listeners('change').length).to.equal(1); | ||
|
||
app.unmount(); | ||
|
||
expect(barStore.listeners('change').length).to.equal(0); | ||
expect(fooStore.listeners('change').length).to.equal(0); | ||
}); | ||
it('should listen to store changes', () => { | ||
const { app } = renderComponent(DumbComponent); | ||
const button = app.root.findByProps({ id: 'button' }) | ||
const foo = app.root.findByProps({ id: 'foo' }) | ||
const bar = app.root.findByProps({ id: 'bar' }) | ||
|
||
button.props.onClick(); | ||
console.error(foo) | ||
|
||
expect(foo.props.children).to.equal('barbar'); | ||
expect(bar.props.children).to.equal('bazbaz'); | ||
}); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need to use useLayoutEffect and not just useEffect?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The original test would fail for useEffect. I think that the following would take place with useEffect:
This causes the assertions in 2 to fail.
With useLayoutEffect:
Basically, it is just to ensure the same behavior as connectToStore.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it. Could you please take a look how react-redux is handling this issue: https://blog.isquaredsoftware.com/2018/11/react-redux-history-implementation/#connect-is-implemented-using-hooks
They are also using useLayoutEffect but on SSR they use useEffect to prevent the warning message.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is how they do: https://github.com/reduxjs/react-redux/blob/adade2069061e09eaa046b10389aee265048e034/src/components/connect.tsx#L56