Skip to content

Latest commit

 

History

History
798 lines (682 loc) · 20.3 KB

README.md

File metadata and controls

798 lines (682 loc) · 20.3 KB

Rax

The fastest way to build universal application.

gzip size


🎄 Familiar: React compatible API with Class Component and Hooks.

🍬 Tiny: ~7 KB minified + gzipped.

🌏 Universal: works with DOM, Weex, Node.js, Mini-program, WebGL and could work more container that implements driver specification.

🍌 Easy: using via rax-cli with zero configuration, one codebase with universal UI toolkit & APIs.

🍭 Friendly: developer tools for Rax development.



Quick Start

Install via NPM

Quickly add rax to your project:

$ npm install rax
$ npm install driver-dom

Starter template

// Hello.jsx
import {createElement, useState} from 'rax';

export default (props) => {
  const [name, setName] = useState(props.name);
  const handleClick = () => {
    setName('rax');
  };
  return (
    <h1 onClick={handleClick}> Hello {name}</h1>
  );
}
// app.jsx
import {render} from 'rax';
import DriverDOM from 'driver-dom';
import Hello from './Hello';

render(<Hello name="world" />, document.body, { driver: DriverDOM });

Using via CLI

Install the Rax CLI tools to init project:

$ npm install rax-cli -g
$ rax init <YourProjectName>

Start local server to launch project:

$ cd YourProjectName
$ npm run start

Using via CDN

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Hello World</title>
    <script src="https://unpkg.com/[email protected]/dist/rax.js"></script>
    <script src="https://unpkg.com/[email protected]/dist/driver-dom.js"></script>
    
    <!-- Don't use this in production: -->
    <script src="https://unpkg.com/[email protected]/babel.min.js"></script>
  </head>
  <body>
    <div id="root"></div>
    <script type="text/babel">
      // @jsx Rax.createElement
      Rax.render(
        <h1>Hello, world!</h1>,
        document.getElementById('root'),
        { driver: DriverDOM }
      );
    </script>
  </body>
</html>

Guides

Server-side rendering and hydration

Use renderToString() to generate HTML on the server and send the markup down on the initial request for faster page loads and to allow search engines to crawl your pages for SEO purposes.

// MyComponent.jsx
function MyComponent(props) {
  return <h1>Hello world</h1>;
}
import express from 'express';
import renderer from 'rax-server-renderer';
import {createElement} from 'rax';
import MyComponent from './MyComponent';

const app = express();
app.listen(8080);

app.get('/hello', (req, res) => {
  let html = renderer.renderToString(createElement(MyComponent));
  res.send(`<!DOCTYPE html><html><body>${html}</body></html>`);
});

If you call hydrate() on a node that already has this server-rendered markup, Rax will preserve it and only attach event handlers, allowing you to have a very performant first-load experience.

import hydrate from 'rax-hydrate';
import MyComponent from './MyComponent';

hydrate(<MyComponent />, document.body);

App Router

Use useRouter to config routing rules, each route map to a component.

import { createElement, Fragment } from 'rax';
import { useRouter, push } from 'rax-use-router';
import { createHashHistory } from 'history';
import Foo from './Foo';
import NotFound from './NotFound';

const config = () => {
  return {
    history: createHashHistory(),
    routes: [
      // Static Component
      {
        path: '',
        component: <>
          <button onClick={() => push('/home')}>go home</button>
          <button onClick={() => push('/404')}>go 404</button>
        </>,
      },
      {
        path: '/home',
        routes: [
          // Dynamic Component 
          {
            path: '',                  // www.example.com/home
            component: () => <>
              <button onClick={() => push('/foo')}>go foo</button>
              <button onClick={() => push('/bar')}>go bar</button>
              <button onClick={() => push('/home/jack')}>go jack</button>
            </>,
          },
          // URL Parameters
          {
            path: '/:username',        // www.example.com/home/xxx
            component: (params) => <>
              <p>{params.username}</p>
              <button onClick={ () => push('/home') }>Go home</button>
            </>
          }
        ]
      },
      // Code Splitting
      {
        path: '/bar',
        routes: [
          {
            path: '',                 // www.example.com/bar
            component: () => import(/* webpackChunkName: "bar" */ './Bar').then((Bar) => {
              // interop-require see: https://www.npmjs.com/package/interop-require
              Bar = Bar.__esModule ? Bar.default : Bar;
              // return a created element
              return <Bar />;
            }),
          },
        ],
      },
      {
        path: '/foo',                 // www.example.com/foo
        component: () => <Foo />,  
      },
      // No match (404)
      {
        component: () => <NotFound />,
      }
    ]
  }
};

export default function Example() {
  const { component } = useRouter(config);
  return component;
}
// Foo.jsx
import { createElement } from 'rax';
import { push } from 'rax-use-router';

export default function Foo() {
  return <button onClick={ () => push('/home') }>Go home</button>
}
// Bar.jsx
import { createElement } from 'rax';
import { push } from 'rax-use-router';

export default function Bar() {
  return <button onClick={ () => push('/home') }>Go home</button>
}
// NotFound.jsx
import { createElement } from 'rax';
import { replace } from 'rax-use-router';

export default function NotFound() {
  return <button onClick={ () => replace('/home') }>Go home</button>
}

Asynchronous Operation

import { createElement, useMemo } from 'rax';
import usePromise from 'rax-use-promise';

const fetchData = () => fetch('https://httpbin.org/get').then(res => res.json());

function Example() {
  const [data, error] = usePromise(useMemo(fetchData));
  if (error) {
    return <p>error</p>
  } else if (data) {
    return <p>{data.foo}</p>
  }
}

Fetch Data

import { createElement } from 'rax';
import useFetch from 'rax-use-fetch';

function Example() {
  const [data, error] = useFetch('https://httpbin.org/get');
  if (error) {
    return <p>error</p>;
  } else if (data) {
    return <p>{data.foo}</p>;
  } else {
    return <p>loading</p>;
  }
}

Code Splitting

Code-Splitting allows you to split your code into various bundles which can then be loaded on demand or in parallel. It can be used to achieve smaller bundles and control resource load prioritization which, if used correctly, can have a major impact on load time.

Code-Splitting is supported by Webpack which can create multiple bundles that can be dynamically loaded at runtime.

import { createElement } from 'rax';
import useImport from 'rax-use-import';

export default function App() {
  const [Bar, error] = useImport(() => import(/* webpackChunkName: "bar" */ './Bar'));
  if (error) {
    return <p>error</p>;
  } else if (Bar) {
    return <Bar />
  } else {
    return <p>loading</p>;
  }
}

Testing

rax-test-renderer provides an renderer that can be used to render Rax components to pure JavaScript objects, without depending on the DOM or a native mobile environment:

import {createElement} from 'rax';
import renderer from 'rax-test-renderer';

const tree = renderer.create(
  <Link page="https://example.com/">Example</Link>
);

console.log(tree.toJSON());
// { tagName: 'A',
//   attributes: { href: 'https://example.com/' },
//   children: [ 'Example' ] }

You can also use Jest's snapshot testing feature to automatically save a copy of the JSON tree to a file and check in your tests that it hasn't changed: http://facebook.github.io/jest/blog/2016/07/27/jest-14.html.

import {createElement} from 'rax';
import renderer from 'rax-test-renderer';

test('Link renders correctly', () => {
  const tree = renderer.create(
    <Link page="https://example.com">Example</Link>
  ).toJSON();
  expect(tree).toMatchSnapshot();
});

Developer Tools

You can inspect and modify the state of your Rax components at runtime using the React Developer Tools browser extension.

  1. Install the React Developer Tools extension
  2. Import the "rax/lib/devtools" module in your app
import 'rax/lib/devtools';
  1. Set process.env.NODE_ENV to 'development'
  2. Reload and go to the 'React' tab in the browser's development tools

React compatibility

Add an alias for react and react-dom in webpack config that makes React-based modules work with Rax, without any code changes:

{
  // ...
  resolve: {
    alias: {
      'react': 'rax/lib/compat',
      'react-dom': 'rax-dom'
    }
  }
  // ...
}

Use TypeScript

Install TypeScript:

$ npm install typescript --save-dev

Install TypeScript loader for webpack:

$ npm install ts-loader --save-dev

Create or update webpack.config.js:

module.exports = {
  mode: "development",
  devtool: "inline-source-map",
  entry: "./app.tsx",
  output: {
    filename: "bundle.js"
  },
  resolve: {
    // Add `.ts` and `.tsx` as a resolvable extension.
    extensions: [".ts", ".tsx", ".js"]
  },
  module: {
    rules: [
      // all files with a `.ts` or `.tsx` extension will be handled by `ts-loader`
      { test: /\.tsx?$/, loader: "ts-loader" }
    ]
  }
};

Add a tsconfig.json file:

{
  "compilerOptions": {
    "sourceMap": true,
    "jsx": "react",
    "jsxFactory": "createElement"
  }
}

Write your first TypeScript file using Rax:

// app.tsx
import { createElement, render } from 'rax';
import * as DriverDOM from 'driver-dom';

interface HelloProps { compiler: string; framework: string; }

const Hello = (props: HelloProps) => <h1>Hello from {props.compiler} and {props.framework}!</h1>;

render(<Hello compiler="TypeScript" framework="React" />, document.body, { driver: DriverDOM});

API Reference

Creating Elements

  • createElement(type, [props], [...children])
createElement('div', { id: 'foo' }, createElement('p', null, 'hello world'));

Fragments

  • Fragment
    <Fragment>
      <header>A heading</header>
      <footer>A footer</footer>
    </Fragment>

Refs

  • createRef()
    const inputRef = createRef();
    function MyComponent() {
      return <input type="text" ref={inputRef} />;
    }
  • forwardRef()
    const MyButton = forwardRef((props, ref) => (
      <button ref={ref}>
        {props.children}
      </button>
    ));
    
    // You can get a ref directly to the DOM button:
    const ref = createRef();
    <MyButton ref={ref}>Click me!</MyButton>;

Hooks

  • useState()
    function Example() {
      // Declare a new state variable, which we'll call "count"
      const [count, setCount] = useState(0);
    
      return (
        <div>
          <p>You clicked {count} times</p>
          <button onClick={() => setCount(count + 1)}>
            Click me
          </button>
        </div>
      );
    }
  • useEffect()
    function Example() {
      const [count, setCount] = useState(0);
    
      // Similar to componentDidMount and componentDidUpdate:
      useEffect(() => {
        document.title = `You clicked ${count} times`;
      });
    
      return (
        <div>
          <p>You clicked {count} times</p>
          <button onClick={() => setCount(count + 1)}>
            Click me
          </button>
        </div>
      );
    }
  • useLayoutEffect()
    function Example() {
      const [count, setCount] = useState(0);
    
      useLayoutEffect(() => {
        // Fires in the same phase as componentDidMount and componentDidUpdate
      });
    
      return (
        <div>
          <p>You clicked {count} times</p>
          <button onClick={() => setCount(count + 1)}>
            Click me
          </button>
        </div>
      );
    }
  • useContext()
    // Create a Context
    const NumberContext = createContext();
    
    function Example() {
      const value = useContext(NumberContext);
      return <div>The answer is {value}.</div>;
    }
  • useRef()
    function TextInputWithFocusButton() {
      const inputEl = useRef(null);
      const onButtonClick = () => {
        // `current` points to the mounted text input element
        inputEl.current.focus();
      };
      return (
        <>
          <input ref={inputEl} type="text" />
          <button onClick={onButtonClick}>Focus the input</button>
        </>
      );
    }
  • useCallback()
    const memoizedCallback = useCallback(
      () => {
        doSomething(a, b);
      },
      [a, b],
    );
  • useMemo()
    const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
  • useReducer()
    const initialState = {count: 0};
    
    function reducer(state, action) {
      switch (action.type) {
        case 'reset':
          return initialState;
        case 'increment':
          return {count: state.count + 1};
        case 'decrement':
          return {count: state.count - 1};
        default:
          // A reducer must always return a valid state.
          // Alternatively you can throw an error if an invalid action is dispatched.
          return state;
      }
    }
    
    function Counter({initialCount}) {
      const [state, dispatch] = useReducer(reducer, {count: initialCount});
      return (
        <>
          Count: {state.count}
          <button onClick={() => dispatch({type: 'reset'})}>
            Reset
          </button>
          <button onClick={() => dispatch({type: 'increment'})}>+</button>
          <button onClick={() => dispatch({type: 'decrement'})}>-</button>
        </>
      );
    }
  • useImperativeHandle()
    function FancyInput(props, ref) {
      const inputRef = useRef();
      useImperativeHandle(ref, () => ({
        focus: () => {
          inputRef.current.focus();
        }
      }));
      return <input ref={inputRef} />;
    }
    FancyInput = forwardRef(FancyInput);

Performance

  • memo()
    function MyComponent(props) {
      /* render using props */
    }
    function areEqual(prevProps, nextProps) {
      /* 
        return true if passing nextProps to render would return
        the same result as passing prevProps to render,
        otherwise return false
      */
    }
    export default memo(MyComponent, areEqual);

Rendering Elements

  • render(element [, container] [, options] [, callback])
    render(<HelloMessage name="world" />, document.body, { driver: DomDriver })

Class Component

  • Component
  • PureComponent

Version

  • version

rax-children

  • Children
    • Children.map(children, function[(thisArg)])
    • Children.forEach(children, function[(thisArg)])
    • Children.count(children)
    • Children.only(children)
    • Children.toArray(children)

rax-proptypes

  • PropTypes
    • PropTypes.array
    • PropTypes.bool
    • PropTypes.func
    • PropTypes.number
    • PropTypes.object
    • PropTypes.string
    • PropTypes.symbol
    • PropTypes.element
    • PropTypes.node
    • PropTypes.any
    • PropTypes.arrayOf
    • PropTypes.instanceOf
    • PropTypes.objectOf
    • PropTypes.oneOf
    • PropTypes.oneOfType
    • PropTypes.shape

rax-is-valid-element

  • isValidElement(object)

rax-clone-element

  • cloneElement(element, [props], [...children])

rax-create-factory

  • createFactory(type)

rax-create-portal

  • createPortal(child, container)

rax-hydrate

  • hydrate(element, container[, callback])

rax-find-dom-node

  • findDOMNode(component)

rax-unmount-component-at-node

  • unmountComponentAtNode(container)

Legacy API

rax-create-class

  • createClass()

Contributing

Want to file a bug, contribute some code, or improve documentation? Excellent! Read up on our guidelines for contributing.

Core Team


@yuanyan

Core


@imsobear

Development


@yacheng

Universals & Components


@boiawang

Loaders & Plugins


@wssgcg1213

DSL Runtimes & Loaders

Users


⬆ back to top