Custom components for Ember, inspired by React Hooks approach
MIT License
Addon used to experiment with React Hooks
style APIs in Ember apps via
existing public APIs.
ember install hooks-component
This addon provide 2 DIFFERENT - API's
The hooks-component
API supports public React HooksAPI
useEffect
-> just like in React APIuseState
-> just like in React APIuseLayoutEffect
-> just like in React APIgetService
-> getService(serviceName)
-> service lookup hookgetController
-> getController(serviceName)
-> controller lookup hookgetRoute
-> getRoute(routeName)
-> route lookup hookgetStore
-> store service lookupgetOwner
-> getOwner()
-> equals getOwner(this)
in Ember.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>
getContextId
-> getContextId()
-> get current instance context id (same between rerenders)getRerender
-> return binded to current instance update
functionaddBeforeCallTask
-> execute some callback before component update
addBeforeDestroyTask
-> execute some callback before any component destroy
// 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);
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.
// 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>
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)
Current hookedComponents implementation logic:
component function
only once, in component creation time.component function
accept named params (args
) as first argument, and return context object
.updateContext
method invoke existing effects and then, do setProperties(currentContext, updatedProps)
.args
updated, it invokes updateContext
method with updated args
.useEffect
method adds "after updateContext
and before setProperties
callbacks with updatedProps
object as argument";useEffect
call return function, it will be called before this effect call next time.updateContext
inside useEffect
don't reinvoke effects, just patching updatedProps
with new data.git clone <repository-url>
cd hooks-component
yarn install
yarn lint:js
yarn lint:js --fix
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 versionsember serve
For more information on using ember-cli, visit https://ember-cli.com/.
This project is licensed under the MIT License.