Using with React.js

It's assumed you have read and are familiar with the Basics guide. If not, then please go back and read through that before trying to learn how to use tao.js with React.

While tao.js itself is client-agnostic, we provide packages to make it seemlessly work with client UI libraries and frameworks.

To start we only have an implementation for React.js. Upcoming and asking for volunteers to help with packages to integrate:

tao.js works seamlessly well with React given the philosophy of building reactive applications at the heart of building applications with tao.js.

To work with React, we make sure to install the @tao.js/react package:

npm install --save @tao.js/core @tao.js/react

@tao.js/core is listed as a peerDependency for the react package so you must install that as well or the package won't work.

This is the second stab at a set of components to work with React. The original version was a very simple way to get a working version of a React-based library out the door. However, that version was much more TAO-centric than it was React-centric. You're more than welcome to use the original version if you like it better (it is simpler).

The new version provides a declarative API that is more React-like that developers have come to expect when building applications using React, leveraging the React Context API underneath so you must be using at least version 16 of React.

The package provides 4 Components and 1 Higher-Order Component (HOC) we can mix and match to integrate tao.js within our React UI:

  • Provider - Component that provides context to the other components in the package so that they can interact with the TAO (or individual Kernel) and shared root data store
  • RenderHandler - a Component that is also a TAO handler that responds to AppCons by rendering its function as a child using the same signature as a regular TAO handler
  • SwitchHandler - a Component that is also a TAO handler used to wrap a set of RenderHandlers to decide which RenderHandlers to include in the view.
  • withContext - a Higher-Order Component (HOC) that turns the Component it is wrapping into a TAO handler that will receive data from AppCons
  • DataHandler - a Component that is also a TAO handler used to capture data from AppCons in response to configured Trigram(s) and make the data available to all child & descendant components for consumption

Example Usage

Here's an example we'll progress through using the various Component exports from the @tao.js/react package to control display of Components and provide them with data based on the AppCons generated during the lifecycle of the application. We'll use the same Example Application to illustrate integrating React here.

Example Directory Structure

Part of the Application deals with Spaces so it has the following directory:

src/
+- components/
   +- Space/
      - index.js
      - Form.js
      - List.js
      - View.js
   +- shared/
      - Welcome.js
- App.css
- App.js

Defining some React Components to use in our examples

The Component definitions for Welcome, Form, List and View define basic React Components (both functional and class) and are not aware of nor dependent upon the @tao.js/react package.

They are making use of the TAO export from @tao.js/core in order to set the Application Context.

src/components/space/View.js

Here is an example of a functional Component:

import React from 'react';
import TAO from '@tao.js/core';

const SpaceView = ({ Space }) => (
  <div>
    <h1>Space - {Space.name}</h1>
    <button
      onClick={e =>
        TAO.setCtx({ t: 'Space', a: 'Edit', o: 'Portal' }, { Space })
      }
    >
      Edit
    </button>
    <button onClick={e => TAO.setCtx({ t: 'Space', a: 'Find', o: 'Portal' })}>
      Back to List
    </button>
    <p>{Space.description}</p>
  </div>
);

export default SpaceView;

Our SpaceView component expects to receive a Space prop which is the Space object it is to display.

The SpaceView has 2 buttons used to change the user's context of the application by setting an AppCon on the TAO to either edit the current Space or find all of the Spaces again and list them.

src/components/space/Form.js

Here is an example full class Component for a simple form used to Edit a Space or Create a new Space.

import React, { Component } from 'react';
import TAO from '@tao.js/core';

class SpaceForm extends Component {
  constructor(props) {
    super(props);
    this.state = Object.assign(
      {
        name: '',
        description: ''
      },
      props.Space
    );
  }

  handleChange = event => {
    const target = event.target;
    const val = target.type === 'checkbox' ? target.checked : target.value;
    const name = target.name;
    this.setState({
      [name]: val
    });
  };

  handleSubmit = event => {
    const { editing } = this.props;
    const Space = this.state;
    const saveAction = editing ? 'Update' : 'Add';
    TAO.setCtx({ t: 'Space', a: saveAction, o: 'Portal' }, { Space });
    event.preventDefault();
  };

  handleCancel = event => {
    event.preventDefault();
    const { Space: { _id } = {} } = this.props;
    TAO.setCtx({ t: 'Space', a: 'Find', o: 'Portal' }, { Find: { _id } });
  };

  render() {
    const { editing } = this.props;
    const Space = this.state;
    return (
      <div>
        <h1>
          {a} Space {Space.name ? `- ${Space.name}` : ''}
        </h1>
        <form name={`${a}Space`} onSubmit={this.handleSubmit}>
          {editing ? <input type="hidden" name="_id" value={Space._id} /> : null}
          <label htmlFor="name">Name:</label>
          <input
            type="text"
            name="name"
            value={Space.name}
            onChange={this.handleChange}
          />
          <br />
          <label htmlFor="description">Description:</label>
          <textarea
            name="description"
            value={Space.description}
            onChange={this.handleChange}
          />
          <br />
          <input type="submit" value="Save" />
          &nbsp;
          <button onClick={this.handleCancel}>Cancel</button>
        </form>
      </div>
    );
  }
}

export default SpaceForm;

Our SpaceForm component expects to receive 2 props:

  • editing telling it whether the form is editing an existing Space or creating a new one
  • Space (optional) the space object being edited

The SpaceForm defines a form that will manage input changes using local state, and then persist those changes by setting an AppCon on the TAO in its handleSubmit method.

Including Space Components in the App

Before we start to wire up our Space Components to be controlled through the TAO, we'll assume a default export from the components/space directory and include it in our App so users can view and interact with Spaces in the app.

We will surround our app with a Provider from the @tao.js/react package that will be used to define which TAO Kernel (in this case the default TAO export) that the Component handlers we define further down in the component tree will be attached to and set up a shared data context available to those handlers.

src/App.js

import React, { Component } from 'react';
import TAO from '@tao.js/core';
import { Provider } from '@tao.js/react';
import logo from './logo.svg';
import './App.css';
import SpaceContainer from './components/space';

class App extends Component {
  render() {
    return (
      <Provider TAO={TAO}>
        <div className="App">
          <header className="App-header">
            <img src={logo} className="App-logo" alt="logo" />
            <h1 className="App-title">Welcome to Mypos</h1>
          </header>
          <SpaceContainer />
        </div>
      </Provider>
    );
  }
}

export default App;

Wiring up Component Handlers for Display Rendering

Basic Rendering with RenderHandler

First we will use the RenderHandler to add our Space components to the render tree when our application encounters specific AppCons.

src/components/space/index.js

import React, { Fragment } from 'react';
import TAO, { AppCtx } from '@tao.js/core';
import {
  RenderHandler,
} from '@tao.js/react';
import List from './List';
import View from './View';
import Form from './Form';

// chain entering a Space with showing the View
TAO.addInlineHandler(
  { t: 'Space', a: 'Enter', o: 'Portal' },
  (tao, { Space }) => {
    return new AppCtx('Space', 'View', 'Portal', { Space });
  }
);

const SpaceContainer = props => (
  <Fragment>
    <h2 class="title">Spaces</h2>
    <RenderHandler term="Space" action="List" orient="Portal">
      {(tao, data) => <List data={data.Space} />}
    </RenderHandler>
    <RenderHandler term="Space" action="View" orient="Portal">
      {(tao, data) => <View Space={data.Space} />}
    </RenderHandler>
    <RenderHandler term="Space" action="Edit" orient="Portal">
      {() => <div>You must save for changes to take effect.</div>}
    </RenderHandler>
    <RenderHandler term="Space" action={['New', 'Edit'] orient="Portal"}>
      {(tao, data) => <Form Space={data.Space} editing={tao.a === 'Edit'} />}
    </RenderHandler>
    <RenderHandler term="Space" action="View" orient="Portal">
      {(tao, data) => <View Space={data.Space} />}
    </RenderHandler>
  </Fragment>
);

export default SpaceContainer;

The SpaceContainer uses a RenderHandler to wrap each of our different space Components to be rendered based on when specific AppCons are encountered. The Trigram for each handler is defined using the term, action and orient props passed to the RenderHandler, and the RenderHandler has a function as a child with the exact same signature as any other TAO handler allowing us to return what we want rendered in the DOM tree when the corresponding AppCon has been encountered.

All Component handlers from the @tao.js/react package are added to the TAO as Inline Handlers. Any ommitted Trigram props (term, action or orient) will be treated as a wildcard when adding the handler to the TAO just like when adding any normal handler to the TAO.

Additionally, you'll notice the action prop used in the RenderHandler for the Form component which provides an array of actions. As a convenience, the RenderHandler (and all other Component handlers from the @tao.js/react package) will compute a cartesian product of all possible Trigrams from the Trigram prop values provided and add itself as a handler for each of them.

Improved Control with SwitchHandler

RenderHandler components behave just like a normal TAO handler reacting to an AppCon but they do not react to other AppCons to which they are not configured to handle. In React terms, this translates to adding their children to the render tree once their configured AppCon has been encountered in the TAO, but not controlling removal. This means the SpaceContainer we defined above will progressively show each of our Space components as users interact with the app, but none of these components will disappear.

To have the selective rendering behavior we need, we'll convert the SpaceContainer into using a SwitchHandler to ensure only the desired space Components we want displayed are rendered and will be removed when the application has moved on from the configured AppCons.

modified src/components/space/index.js

import React from 'react';
import TAO, { AppCtx } from '@tao.js/core';
import {
  SwitchHandler,
  RenderHandler,
} from '@tao.js/react';
import List from './list';
import View from './view';
import Form from './form';

// chain entering a Space with showing the View
TAO.addInlineHandler(
  { t: 'Space', a: 'Enter', o: 'Portal' },
  (tao, { Space }) => {
    return new AppCtx('Space', 'View', 'Portal', { Space });
  }
);

const SpaceContainer = props => (
  {/* surround RenderHandlers with a SwitchHandler */}
  <SwitchHandler term="Space" orient="Portal">
    <h2 class="title">Spaces</h2>
    <RenderHandler action="List">
      {(tao, data) => <List data={data.Space} />}
    </RenderHandler>
    <RenderHandler action="View">
      {(tao, data) => <View Space={data.Space} />}
    </RenderHandler>
    <RenderHandler action="Edit">
      {() => <div>You must save for changes to take effect.</div>}
    </RenderHandler>
    <RenderHandler action={['New', 'Edit']}>
      {(tao, data) => <Form Space={data.Space} editing={tao.a === 'Edit'} />}
    </RenderHandler>
    <RenderHandler action="View">
      {(tao, data) => <View Space={data.Space} />}
    </RenderHandler>
  </SwitchHandler>
);

export default SpaceContainer;

The SwitchHandler is now providing control over its child RenderHandlers by determining based on the AppCons it sees which should be included in the render tree.

To reduce verbosity, the SwitchHandler accepts Trigram props that act as default Trigram props for all child RenderHandlers and will be passed into the child RenderHandlers when they're added to the render tree. If a Trigram prop is defined by both, the RenderHandler's value will override the SwitchHandler's value both in determining the Trigram of the TAO handler the SwitchHandler uses and on the RenderHandler component added to the render tree.

The configuration of surrounding RenderHandlers with a SwitchHandler is to provide exclusivity aka selection around which RenderHandlers to display. It isn't necessary for all RenderHandlers to be surrounded by a SwitchHandler, especially if the children of the RenderHandler can control their own display (like we'll see with the ErrorMessage.

The SwitchHandler only manages selective rendering for RenderHandler direct children, meaning any descendant RenderHandlers are unaffected by an ancestor SwitchHandler. It also means the SwitchHandler will render any other children (Components or text) into the tree (e.g. the h2 title will always be in the render tree as long as the SpaceContainer is in the render tree).

Getting data from AppCons into Components

Using withContext HOC

The withContext Higher-Order Component can be used to turn any React Component into a TAO handler that will receive data from an AppCon when it is encountered.

As a very simple example, say we want to have a welcome message for users of the app.

src/components/shared/Welcome.js

import React from 'react';
import { withContext } from '@tao.js/react';

const Welcome = ({ data }) => (
  <h2>Welcome{data.user ? ', ' + data.user.displayName : ' to Mypos'}!</h2>
);

export default withContext(
  { t: 'User', a: 'Enter', o: 'Portal' },
  (tao, data) => ({ user: data.User }),
  { user: null }
)(Welcome);

The withContext HOC wraps our Welcome component to provide the User data when it is encountered during a {User,Enter,Portal} AppCon.

The first arg to withContext defines the Trigram that our handler will be looking for.

The second arg to withContext is the TAO handler that will be called when the matching AppCon is set on the TAO with a variation from a standard TAO handler: you can return a value that will be used to determine the shape of the data prop that withContext will pass to the Welcome component.

The third optional arg to withContext is used to set the default value of the data prop before any AppCons are encountered.

Now our global header or App component can import the default export from src/components/shared/welcome.js and embed it in the page within a Provider and it'll just work like any other React Component.

src/App.js with Welcome

import React, { Component } from 'react';
import TAO from '@tao.js/core';
import { Provider } from '@tao.js/react';
import logo from './logo.svg';
import './App.css';
import SpaceContainer from './components/space';
import Welcome from './components/shared/welcome';

class App extends Component {
  render() {
    return (
      <Provider TAO={TAO}>
        <div className="App">
          <header className="App-header">
            <img src={logo} className="App-logo" alt="logo" />
            <Welcome />
          </header>
          <SpaceContainer />
        </div>
      </Provider>
    );
  }
}

export default App;

The withContext HOC has much more flexibility that you can find in the doc page.

Using DataHandlers for shared state

DataHandlers provide a way to wire up TAO handlers into your React Component tree that capture data and make it available to child and decendant components down the hierarchy.

The Provider sets up the initial data context as a map, and DataHandlers will modify that map when they react to their configured AppCons.

Let's say we want to classify Spaces using a type field so we need a list of types as a lookup. We'll modify our SpaceContainer to capture this list and make it available to it's children.

src/components/space/index.js with types

import React from 'react';
import TAO, { AppCtx } from '@tao.js/core';
import {
  SwitchHandler,
  RenderHandler,
} from '@tao.js/react';
import List from './List';
import View from './View';
import Form from './Form';

// chain entering a Space with showing the View
TAO.addInlineHandler(
  { t: 'Space', a: 'Enter', o: 'Portal' },
  (tao, { Space }) => {
    return new AppCtx('Space', 'View', 'Portal', { Space });
  }
);

// fetch the SpaceTypes when we enter the App
TAO.addAsyncHandler(
  { t: 'App', a: 'Enter', o: 'Portal' },
  () => new AppCtx('SpaceType', 'Find', 'Portal'),
);

const SpaceContainer = props => (
  {/* Surround our tree with our DataHandler */}
  <DataHandler
    name="spaceTypes"
    term="SpaceType"
    action="List"
    orient="Portal"
    default={[]}
    handler={(tao, data) => data.SpaceTypes}
  >
    <SwitchHandler term="Space" orient="Portal">
      <h2 class="title">Spaces</h2>
      <RenderHandler action="List">
        {(tao, data) => <List data={data.Space} />}
      </RenderHandler>
      <RenderHandler action="View">
        {(tao, data) => <View Space={data.Space} />}
      </RenderHandler>
      <RenderHandler action="Edit">
        {() => <div>You must save for changes to take effect.</div>}
      </RenderHandler>
      {/* Reference the DataHandler's data using the `context` prop */}
      <RenderHandler action={['New', 'Edit']} context="spaceTypes">
        {/* the context we referenced will augment the handler with args passing the data */}
        {(tao, data, spaceTypes) => (
          {/* pass the spaceTypes into the Form via the types prop */}
          <Form Space={data.Space} editing={tao.a === 'Edit'} types={spaceTypes} />
        )}
      </RenderHandler>
      <RenderHandler action="View">
        {(tao, data) => <View Space={data.Space} />}
      </RenderHandler>
    </SwitchHandler>
  </DataHandler>
);

export default SpaceContainer;

Above we've added a few changes to our space/index.js and SpaceContainer component without having to create new components or new files.

First, we add a new Async Handler to fetch the SpaceTypes lookup data when entering the app.

Next we add a DataHandler component to react to the {SpaceType,List,Portal} AppCon which will be triggered when successfully fetching the SpaceTypes from the backend. The DataHandler's handler is defined to return just the list of SpaceTypes in the AppCon data, which then sets that value in the data context from the closet ancestor Provider in the component hierarchy using the name prop of the DataHandler (in this case 'spaceTypes').

We get access to this data lower down in the hierarchy when we define the context prop on the RenderHandler surrounding our space Form (context="spaceTypes"). When we add the context prop to the RenderHandler, it finds the data in the data context under the key(s) [multiple contexts may be provided using an Array of context names - see the RenderHandler docs] and append that data as args to the handler used as the function as a child of the RenderHandler (if multiple contexts are provided, then each is appended as an arg in the order in which they're listed in the prop value).

Finally, in the function as a child prop of the RenderHandler we capture the spaceTypes arg and pass it to the Form component as the value of the types prop.

results matching ""

    No results matching ""