react-maestro

0.1.0-alpha • Public • Published

Forget Relay.

What's the point in remaking something that will ALWAYS work better? Not to mention, it is incredibly complex and therefore hard to re-implement.

How can I make this more like a Radar + Cargo XHR with some Relay-like syntax?

Specifically, declarative Components are a GREAT idea. As is wrapping the Store with a Network layer and automatically inserting data returned by a Query into a central Store.

What do we do when we use Radar?

We call the network, then put data from the network into the Store. We do this by calling 'Actions' like receive. On any action, the Components which listen to that Store are alerted to the Action and decide what to do with the new data the Store passes them.

Perhaps our Actions can include a callback which connects to the Store. That way, you have direct access to the Records within the Store.

Schemas, Declarations, Queries, Stores and Actions. These are the concepts we need to familiarize ourselves with from here-on out.

What is a Schema?

  1. Defines the exact fields that exist within a model of data.
  2. Optionally casts the return data from the network to a JS type. This is useful for integrating things like moment.js easily.
  3. Has a require method which allows Components to declare specific data from it.
  4. That's it. That's all.
const FooSchema = createSchema({
  // The name of the Schema on the backend
  name: 'FooSchema',
  // The fields contained within the Schema
  fields: {
    // Normal field casting is optional. By using 'null' you're telling
    // the Schema to trust the backend to cast correctly to JS.
    foo: fields.String,
    bar: {
      baz: fields.Int,
      // You can include your own typecaster
      barbaz: myOwnFields.Moment
    }
  }
})
 
 
const FoosSchema = createSchema({
  name: 'FoosSchema',
  fields: {
    // Can be one or many of this node
    foos: FooSchema.fields,
    // Uncasted
    cursor: null
  }
})

What should a Component declare?

  1. The exact fields it needs from its parent Store, as defined by a Schema.
  2. Any data declared by child Components. Any Component containing Components with a declaration will have to declare those child declarations.
  3. That's it. Parameters are declared in the queries alone. Pagination and etc. will be handled by changing parameters on the query, as defined by the user - not an internal system.
@requires({
  foo: () => FooSchema.require(`foo bar {baz}`)
})
class MyChildComponent extends React.Component {
  render () {
    return (<div>Baz: {this.props.foo.bar.baz}</div>)
  }
}
 
 
@requires({
  foos: () => FoosSchema.require(`
    foos [
      ${MyChildComponent.getSchema('foo')}
    ]
  `)
})
class MyComponent extends React.Component {
  get foos () {
    const foos = []
 
    for (let foo of this.props.foos.foos) {
      foos.push(<MyChildComponent foo={foo}/>)
    }
 
    return foos
  }
 
  render () {
    return (<div>{this.foos}</div>)
  }
}

How does a Query work?

  1. A query optionally contains input params which are sent along to the backend.
  2. It also contains information via declarations about what Schemas and fields need to be retrieved.
  3. It is formatted and sent over the network as a JSON POST request. POST is simply the most sensible HTTP request method to use for the request.
  4. Your backend server resolves the Schema required by the query and returns a JSON response containing the resolved data.
  5. Each query contains a user-defined label within the Store. This label is used to manipulate the return contents with Actions.
  6. Each Schema within a query has a user-defined label. This label is used when passing the data returned by the query to the Store's child Components. The label should be the same as the label of the outermost requires referenced in the Component tree.
const FooQuery = createQuery({
  // The name of the query on the backend server
  name: 'FooQuery',
  // The initial parameters defined within the query. The values of these
  // can be updated when the query is constructed within the Store.
  //
  // They can also be updated within Components with declarations. When that
  // happens, the query is re-fetched and its parent Store's child Components
  // are re-rendered.
  initialParams: {
    foo: 'bar',
    limit: 20,
    after: 0
  },
  // The Schemas allowed to be returned by the query
  schemaTypes: [FoosSchema]
})

So what about this Store we keep hearing about?

  1. Connects the data returned by Queries and Actions to a central Record Map.
  2. Automatically updates if Records listened to by child Components change via some other Store or Action.
const FooStore = createStore({
  // Display name of the Store for debugging
  name: 'FooStore',
  // Initial parameters which can be updated via props when the
  // Store is rendered
  params: {
    ...FooQuery.initialParams
  },
  // Queries to be sent over the Network when the Store is rendered
  queries: [
    // 'params' here comes from 'params' defined above
    params => new FooQuery({
      params,
      requires: {
        // 'foos' becomes the prop delivered to the Store's child Components
        foos: MyComponent.getSchema('foos')
      }
    })
  ],
  // Networking options
  network: {
    endpoint: 'http://gomaestro.io/1.0/maestro'
  }
})
 
 
class FoosPage extends React.Component {
  render () {
    return (
      <FooStore limit={20} after={(this.props.router.page * 20) - 20}>
        <Success>
          <MyComponent/>
        </Success>
 
        <Loading>
          <div>
            Loading...
          </div>
        </Loading>
 
        <Failure>
          <div>
            Network request failed.
            <a onClick={this.props.maestro.refresh()}>
              Try again
            </a>
          </div>
        </Failure>
      </FooStore>
    )
  }
}

Finally, how do Actions manipulate the Store state?

  1. Actions have some predefined configurations to easily manipulate the Store once data is returned by the Network - and optimistically.
  • Array actions

    • push: pushes a value to the end of an array at a specified key in the Store from the matching key in the return value
    • unshift: prepends a value to the start of an array at a specified key in the Store from the matching key in the return value
    • concat: concats an array at a specified key in the Store with the matching key from the return value
    • delete: deletes one or several return records from a matching key in the store
  • Object actions

    • assign: assigns the keys in the return to the state of the Store
    • merge: like assign except that it recursively merges own and inherited enumerable string keyed properties of source objects into the destination object. Source properties that resolve to undefined are skipped if a destination value exists. This also works with arrays.
  • Customized actions

    • Your callback receives two arguments, one for the current Records state of a given key and a second for the received Records. Your return value reflects the new Record value of the key
  1. Actions also have the ability to manually manipulate the Store with callback functions, but care needs to be taken with this approach.
class AddFoo extends Action {
  // The name of the action on the backend
  get actionName () {
    return 'AddFoo'
  }
 
  // Data sent as input parameters for the action
  get input () {
    return {
      foo: this.props.foo,
      bar: this.props.bar
    }
  }
 
  // Defines the return schema which needs to be resolved on the backend
  static requires = {
    foo: () => MyChildComponent.getSchema('foo')
  }
 
  // Adds this data to the Store prior to the Network call finishing
  get optimisticSchema () {
    return {
      foo: {
        // __maestroKey__ must be universally unique
        __maestroKey__: this.props.foo,
        foo: this.props.foo
        bar: this.props.bar
      }
    }
  }
 
  // Manipulates the data in the Store itself
  get actions () {
    // Must return an array or single object of actions to undertake
    return  {
      type: Action.PUSH,
      // Manipulates the data returned by the foo key in the requires
      // statement
      actionSchema: 'foo',
      // Period delimited object description indicating the location of the
      // records being manipulated
      storeProp: 'foos.foos'
    }
  }
}
 
 
@requires({
  foo: () => FooSchema.require(`foo bar {baz}`)
})
class MyActionableComponent extends React.Component {
  addFoo = e => {
    // Adds an action to the actions stack. Actions are delivered to the Network
    // and executed in the order which they are received.
    const action = this.props.maestro.update(new AddFoo({
      foo: 'bar',
      bar: {
        baz: 'barbaz'
      }
    }))
 
    // Tells the network to send this action
    // To send all pending actions, call `this.props.maestro.commit()`
    //
    // Committing an action returns a Promise
    action.commit().then(e => console.log(e))
  }
 
  render () {
    return (
      <a onClick={this.addFoo}>
        +Add foo
      </a>
    )
  }
}

Readme

Keywords

none

Package Sidebar

Install

npm i react-maestro

Weekly Downloads

0

Version

0.1.0-alpha

License

MIT

Last publish

Collaborators

  • jaredlunde