-
Notifications
You must be signed in to change notification settings - Fork 131
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
[Meta] v2 API thoughts #337
Comments
A bias towards Redux is not necessaryThe deku v2 API looks like this: import { dom } from 'deku'
const store = /* a redux store-like object */
const render = dom.createRenderer(document.body, store.dispatch)
render(<MyApp />, store.getState()) When used with Redux, it'd be more like this: const store = createStore(...)
const render = dom.createRenderer(document.body, store.dispatch)
const update = () => { render(<MyApp />, store.getState()) }
store.subscribe(update) // re-run render() when new things happen
update() Passing
|
it doesn't need to be an action, and it doesn't need to be sent to a store. The point of dispatch is to channel all UI actions to a single place. It can then act as a proxy, dispatching to more than one place. |
States are a bit more than this. For instance, if I have an export function initialState () {
return { startedAt: new Date() }
}
// if the event handler was given access to state
function onLoad ({ state }) {
var elapsed = +new Date() - state.startedAt
} With the new API, this isn't possible. You can probably trigger a dispatch at export function onCreate({ dispatch, path }) {
// hypothetical non-redux dispatch() that saves UI states
dispatch(path, { loadedAt: +new Date() })
} ...But for any |
The idea is that Deku is lower-level than that. You could use something like raf-debounce to make sure the render function is only called once per frame. I think what would be nice is to have a higher-level module that handles a lot of the wiring and setup for you. It's just outside of the scope of Deku itself. Sort of like how Elm has the This is similar to @ashaffer's vdux: start(app, node, [...middleware])
True, but you also don't have to store the state in Redux. It's just a way to get actions from the UI. You could do something like: let state = {}
let render = createRenderer(node, action => {
if (action.type === 'update state') {
state[action.path] = { ...(state[action.path] || {}), ...action.state }
} else {
store.dispatch(action)
}
scheduleRender()
})
render(<App />, {
state,
store: store.getState()
}) You could even go so far as to have two updates, use Redux to update the context, and use the Elm-style way of passing down an update function for local state. This would allow you to have reducers within each component for their own local state, and use You could also wrap components in a higher-level component that manages pulling UI state from the context and handing it to the component. You'd get an
The only reason I made |
The problem is not dispatch or component states. You can keep local state using your own registry and identifying components by their path. The problem is more that you don't have any mechanism re-render a specific sub-tree or leaf. It is particularly useful for images like mentioned, and also content which needs to change without a data change (when time rather than data is the trigger): look at https://github.com/troch/react-timer-hoc for example. Maybe I am looking at it the wrong way? As you said, deku is quite low-level. Could an application tree have many deku sub-trees? |
An example of the higher-level component that handles state: function stateful (Component) {
let state = {}
let pendingState = {}
function updateState (path, dispatch) {
return state => {
pendingState[path] = state
dispatch({
type: 'state changed'
})
}
}
function onCreate ({ path }) {
// you could even do a Component.initialState call here
state[path] = {}
}
function render ({ path, props, dispatch }) {
state[path] = {...state[path], ...pendingState[path] }
return <Component state={state[path]} updateState={updateState(path, dispatch)} {...props} />
}
function onRemove ({ path }) {
delete state[path]
}
function shouldUpdate ({ path }) {
return path in pendingState
}
return {
render,
onCreate,
onRemove,
shouldUpdate
}
} Then in your component: let App = {
render: ({ props }) => {
return <button onClick={props.updateState({ text: 'world' })}>{props.state.text || 'hello'}</button>
},
onCreate
}
export default stateful(App) Then you just need to handle the This way is a little too magic-y for me, and I'd probably go the Elm route for local state and just pass down an update function that gets reduced the whole way up. |
For the record, even in v1, and in React, the whole tree is just re-rendered on a state change. All it did was just trigger the top-level render function action. Components work as thunks to prevent rendering branches of the tree. With a really basic Remember the only thing you're actually skipping is the diffing. It won't actually apply any changes to the dom and the diffing should be rather quick. You can manually speed up the diffing too by splitting your render function into smaller functions and memoizing them. I'm going to put together some docs for all of this so people can more easily make a choice :) |
Interesting. I guess passing something other than a Function to state = {}
render = createRenderer(document.body, { state: setPathState, store: store.dispatch() })
update = debounce(() => render(<App />, { state, store: store.getStore() })
let setPathState = (path) => (values) => {
state[path] = { ...(state[path] || {}), ...values }
update()
} And in your components: render ({ dispatch, path }) {
const setState = dispatch.state(path)
return <button onClick={onclick(setState)}>
}
let onclick = (setState) => (path) => {
setState({ clicked: true })
} A bit long winded but I'll take it :) We do miss the ability to set an initialState, though. The docs seem to really push Redux in there though, as if passing an object as a dispatcher is something non-kosher. |
There is still something I trip over: in React the whole tree is re-evaluated on each setState / mutation, but i Without having the ability to mark nodes dirty, does it prevent optimizations? Are For example, with redux and react, |
True. I didn't think of that. You could still mark nodes as dirty using their The downside is that if you have a 'pure' component at the top level with a pure I wonder if the function shouldUpdate (component, prev, next) {
// if the state has change for this component or any components within this `path`
// then return true. If prev and next have different props, children, or context, also
// return true.
}
let render = createRenderer(el, dispatch, shouldUpdate) Then you could apply a 'pure' update function as well as a check for state changes in a single spot for your entire UI. |
Another idea, going down the path of memoized render functions:
|
I managed to hack together a function render ({ props, path, context, dispatch }) {
let state = context.ui[path]
let setState = (data) => { dispatch({ type: 'ui:set', path, data }) } and in Redux: const ui = createReducer({
'ui:set': (state, { path, data }) => {
return { ...state, ui: { ...state.ui, [path]: { ...state.ui[path], ...data } }
}
}) and in App: let render = dom.createRenderer(
document.getElementById('_app'),
this.store.dispatch)
let update = () => { render(<AppRoot />, this.store.getState()) }
this.store.subscribe(debounce(update, 0)) |
ohh, not bad.
Mind if I make an npm package out of that concept? |
@rstacruz You may want to take a look at state-lens and redux-ephemeral. The state lens thing is just sort of a rough idea, but it works. |
@ashaffer, nice work on those! I like it. However, if you're using, say, Immutable.js or Baobab or Scour as your redux state, it isn't as easy to integrate. |
@rstacruz Ya, I know that's a problem. I'm planning on allowing you to mixin getProp/setProp to redux-ephemeral to support that use-case. I'll try to get that up today or tomorrow. |
@anthonyshort: the problem with initialState on onCreate in your example, though, is that it triggers after the first render... so your first render has no state.
|
Here you go! https://www.npmjs.com/package/deku-stateful Using it now... works like a charm. (I've also updated decca to be completely 100% compatible with deku's v2 API—that is, no states.) |
Hey, i've had a chance to prototype the v2 api and I'd like to share my thoughts on it.
Stateless is awesome
I really love how Deku's component API isn't made with classes. This was one of the things I disliked about React: if components are classes, when are they constructed? How can I access the component instance? Thinking outside classes really drives in the functional and immutable aspect of the API design.
But removing setState was a bad idea
The biggest change from v2 is the removal of state and instead relying on Redux. I think this can cause a lot of headaches.
React's rendering works something like this:
Effectively, this means 30 components can call
setState()
in one render pass, but re-rendering will only happen once. This is good, because it means we don't re-render 30 times if 30 components want to change states.Using Redux for states
In contrast, deku v2 asks you to dispatch a store action instead. If 30 components want to change states. This means
store.subscribe(() => { ... })
's callbacks will be called 30 times, which means re-rendering 30 times.Also, this dispatching can happen /while/ the tree is rendering, causing problems. Consider this naive example:
This is a bad idea, because it means a re-rendering will happen while the onCreate action is executed. You can mitigate this by using
requestAnimationFrame
orprocess.nextTick
or similar, so that they happen after rendering:But this means DOM updates might happen twice, and the rendering is now needlessly asynchronous.
Use cases of states
Here are some use cases for
setState
:<img>
loaded completely...these transient state flags will only polute the Redux state.
The text was updated successfully, but these errors were encountered: