Awesome
hooks-component
Addon used to experiment with React Hooks
style APIs in Ember apps via
existing public APIs.
Installation
ember install hooks-component
This addon provide 2 DIFFERENT - API's
- React way hooks implementation (always call component function on rerender).
- Ember way hooks implementation (call component function on first render only).
Usage in React-Way
The hooks-component
API supports public React HooksAPI
Builtin hooks
useEffect
-> just like in React APIuseState
-> just like in React APIuseLayoutEffect
-> just like in React API
getService
->getService(serviceName)
-> service lookup hookgetController
->getController(serviceName)
-> controller lookup hookgetRoute
->getRoute(routeName)
-> route lookup hookgetStore
-> store service lookupgetOwner
->getOwner()
-> equalsgetOwner(this)
in Ember.
Example
import { reactComponent, useEffect, useState } from "hooks-component";
function ConferenceSpeakersReact() {
const [ speakers ] = useState(['Tom', 'Yehuda', 'Ed']);
const [ current, updateCurrent ] = useState(0);
useEffect(() => {
console.log('dummy effect');
});
const next = () => {
let nextSpeaker = current + 1;
updateCurrent(nextSpeaker);
}
return {
currentlySpeaking: speakers[current],
moreSpeakers: (speakers.length - 1) > current,
current,
next, speakers
}
}
export default reactComponent(ConferenceSpeakersReact);
{{!-- app/templates/components/conference-speakers-react.hbs --}}
<div>
<p>Speaking: {{this.currentlySpeaking}}</p>
<ul>
{{#each speakers key="@index" as |speaker|}}
<li>{{speaker}}</li>
{{/each}}
</ul>
{{#if this.moreSpeakers}}
<button onclick={{action this.next this.current}}>Next</button>
{{else}}
<p>All finished!</p>
{{/if}}
</div>
How to create custom hooks?
getContextId
->getContextId()
-> get current instance context id (same between rerenders)getRerender
-> return binded to current instanceupdate
functionaddBeforeCallTask
-> execute some callback before componentupdate
addBeforeDestroyTask
-> execute some callback before any componentdestroy
// utils/custom-hook.js
import { getContextId, getRerender, addBeforeCallTask, addBeforeDestroyTask } from "hooks-component";
const DUMMY_STORE = {};
var CALL_COUNTER = 0;
addBeforeCallTask(()=>{
CALL_COUNTER = 0;
});
addBeforeDestroyTask(()=>{
const uid = getContextId();
if (uid in DUMMY_STORE) {
delete DUMMY_STORE[uid];
}
});
export function myCustomHook(componentStoreDefaultValue = {}) {
const uid = getContextId(); // current component instance ID
const hookCallId = CALL_COUNTER; // how many times hook called during rendering
if (!(uid in DUMMY_STORE)) {
DUMMY_STORE[uid] = {}; // init store for component instance;
}
if (!(hookCallId in DUMMY_STORE[uid])) {
// init store for exact call number inside component isntance;
DUMMY_STORE[uid][hookCallId] = componentStoreDefaultValue;
}
// get current instance + callNumber state
let state = DUMMY_STORE[uid][hookCallId];
// get rerender function (must be inside hook)
let rerender = getRerender();
// increment hook call counter
CALL_COUNTER++;
// return current state for exact component and callNumber and update state function
return [ state, function(newState) {
Object.assign(state, newState);
// rerender will invoke component rerender
rerender();
}
}
import { reactComponent } from "hooks-component";
import myCustomHook from "utils/custom-hook";
function ConferenceSpeakersReact() {
const [ state , patchState ] = myCustomHook({ keys: 1 });
const [ fish, patchFish ] = myCustomHook({ salmon: 1 });
const { keys } = state;
const { salmon } = fish;
const next = () => {
patchState({
keys: keys + 1
})
}
const addSalmon = () => {
patchFish({
salmon: salmon + 1
})
}
return { keys, next, salmon }
}
export default reactComponent(ConferenceSpeakersReact);
Usage in Ember-Way
The hooks-component
API supports part of React hooks API, including:
updateContext - just like setProperties;
useEffect - do some calculation after dependent keys changed
extract - just like getWithDefault for component arguments
useEffect
- inside component function
context support: function, tracked property paths in array-like style ['foo.length', 'foo', 'foo.firstObject']
;
All effects called during first render, on rerender effects called only if "tracked" property changed.
Example
// app/components/conference-speakers.js (.ts would also work)
import hookedComponent from "hooks-component";
function ConferenceSpeakers(attrs = {}) {
const { updateContext, useEffect, extract } = this;
useEffect(({current, speakers}) => {
updateContext({
currentlySpeaking: speakers[current],
moreSpeakers: (speakers.length - 1) > current
})
}, ['current'] );
const next = (current) => {
current++;
updateContext({
current
});
}
return extract(attrs, {
next,
current: 0,
speakers: ['Tom', 'Yehuda', 'Ed']
});
}
export default hookedComponent(ConferenceSpeakers);
{{!-- app/templates/components/conference-speakers.hbs --}}
<div>
<p>Speaking: {{currentlySpeaking}}</p>
<ul>
{{#each speakers key="@index" as |speaker|}}
<li>{{speaker}}</li>
{{/each}}
</ul>
{{#if moreSpeakers}}
<button onclick={{action next this.current}}>Next</button>
{{else}}
<p>All finished!</p>
{{/if}}
</div>
useEffect API
function shouldRecomputeEffect(oldObject: object, newObject: object): boolean;
type Tracker = string | object | shouldRecomputeEffect;
type cleanupComputedEffect = undefined | Function;
function computeEffect(newContext: any): cleanupComputedEffect;
function useEffect(computeEffect, trakedItems?: Tracker | Tracker[] , useTrackersOnFirstRender?: boolean = false)
How it's working?
Current hookedComponents implementation logic:
- We run
component function
only once, in component creation time. component function
accept named params (args
) as first argument, and returncontext object
.updateContext
method invoke existing effects and then, dosetProperties(currentContext, updatedProps)
.- if component
args
updated, it invokesupdateContext
method with updatedargs
. useEffect
method adds "afterupdateContext
and beforesetProperties
callbacks withupdatedProps
object as argument";- if
useEffect
call return function, it will be called before this effect call next time. updateContext
insideuseEffect
don't reinvoke effects, just patchingupdatedProps
with new data.
Contributing
Installation
git clone <repository-url>
cd hooks-component
yarn install
Linting
yarn lint:js
yarn lint:js --fix
Running tests
ember test
– Runs the test suite on the current Ember versionember test --server
– Runs the test suite in "watch mode"ember try:each
– Runs the test suite against multiple Ember versions
Running the dummy application
ember serve
- Visit the dummy application at http://localhost:4200.
For more information on using ember-cli, visit https://ember-cli.com/.
License
This project is licensed under the MIT License.