Skip to content
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

Built-in State Management Proposal #18

Open
JAForbes opened this issue Apr 13, 2020 · 2 comments
Open

Built-in State Management Proposal #18

JAForbes opened this issue Apr 13, 2020 · 2 comments
Labels
enhancement New feature or request

Comments

@JAForbes
Copy link
Collaborator

JAForbes commented Apr 13, 2020

Assumptions

  1. Vanilla state management is difficult in vella because S isn't intuitive.
  2. Vella benefits from granular stream updates
  3. Due to 2. single state objects compromise Vella's performance value proposition
  4. Any state management approach we use must be optimized for 2.

Solutions

  1. We should borrow from some of my research on queries+streams.
  2. We can heavily simplify the API for the specific use case of state management within vella.
  3. Query+Streams uses proxy property access so it should feel more "native", and it can be typescript compatible in a similar way to monolite

Examples

// v.boot will provide access to a state query stream
v.boot(document.body, ({ state }) => {

  // you can read the state by invoking it:
  console.log( state() )

  // you can write to the state by invoking it with a parameter
  state( newState )
  
  // you can transform the state by passing in a transform function
  state( prevState => f(prevState) )

  // you can access sub properties of the state with normal property access
  const firstUser = state.users[0]

  // `firstUser` has the exact same api as `state`
  console.log( firstUser() )
  firstUser( newUser )
  firstUser( prevUser => f(prevUser )

  // Both `state` and `firstUser` are S streams that vella natively understands
  // But `firstUser` only "emits" when its value changes

  // this will only re-render when firstUser.name changes value.
  return v('p', 'Hello', firstUser.name)

})

If state wasn't already provided as an attribute, every component will receive state as an attr. The default reference will be the state the parent component received.

function MyComponent({ state }){
  // And state can be rebound to a subtree
  // as easily as aliasing it in the attrs
  return v(OtherComponent, { state: state.subsection })
}

state instances will have a few methods for relational queries.

  • map for mapping over lists
  • find for selecting an item in a list
  • delete for removing an item from a list

These methods are different from attain queries, where operations focus on the query not the underlying value. But vella doesn't need to be so puritanical because it has a specific use case in mind - so it can optimize the api to feel more like working with native JS structures.

An example of .find

// bad - points to a specified index which is brittle
const user = state.users[0]

// good - a dynamic query which focuses 
// on a specific identity relationship - which is resilient
const user = state.users.find( user => user.id == id() )

And .delete()

const user = state.users.find( user => user.id == id() )

// remove from list/object where user.id == id()
user.delete()

A simple counter example (excuse any typos):

v.boot( document.body, ({ state }) => {
  // initialize the state
  state({
    counters: []
  })

  // mount Counters without passing down state
  return v(Counters)
})

// Counters receives the root state as an attr
// automatically
function Counters({ state }){
  
  return v('.counters'
    , v('button'
      , 
      // create a counter on click
      { onclick: () => state.counters( xs => xs.concat( { id: uuid(), count: 0 }) )
      }
      , 'Add Counter'
    )
    // map over the counters (.map here produces a Stream<UUID[]> )
    , v.list( () => state.counters.map( x => x.id ),  id =>
      // create a query on the fly for each component
      v(Counter, { state: state.counters.find( x => x.id == id ) })  
    ) 
  )
}

// state is focused on the counter query 
function Counter({ state }){

  return v('counter'
    // because of transform functions 
    // we don't have that awkward count( count() + 1 )
    // and therefore we can take advantage of most utility toolbelts
    , v('button', { onclick: () => state.count( x => x + 1 ), '+' } 
    , v('button', { onclick: () => state.count( x => x - 1 ), '-' } 

    // Only mutates the dom when 
    // `state.users.find( x => x.id == id ).count` changes
    , v('p.count', 'Count', state.count )

    , v( Delete )
  )
}

// receives the counter as a state query
// even that no attr was specifically passed down
function Delete({ state }){
  // Remove the counter from the list
  return v('button', { onclick: () => state.delete(), 'Delete' }
}
@pygy pygy added the enhancement New feature or request label Apr 15, 2020
@CreaturesInUnitards
Copy link
Collaborator

@JAForbes this seems like a tremendously great approach; almost too good to be true! I'd love to have it in an alpha branch sooner than later. I'm curious what the accompanying persistence idioms would/should look like.

@JAForbes
Copy link
Collaborator Author

Yeah seems like there's a bit of support (even @barneycarroll has come around to it 😂). I'll try and find the time to implement a prototype if there's no objections.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants