component()

This section describes how to create Reactive Components using component() API.

component() creates a react functional component which is bound to the reactive data. It is able to create components in two different ways accepting two different functional component definition. First overload accepts an ordinary react functional component and acts like a higher order component. Second overload accepts "reactive component definition" which is a function that returns a renderer function. Reactive component definition enables a persistent local scope that will be created once for each component and disposed when the component is unmounted, eliminating all the quirks of 'hooks'.

Defining a Reactive Component

This is the recommended way of defining components especially if component has local state or logic. Reactive component structure is very similar to a functional component. component() accepts a function that returns another function returning React node. This allows a closure to be created which persists as long as the component is alive.

import { component } from '@re-active/react';

const MyComponent = component((props) => {

    // ***************
    // Component local scope
    // ***************

    return () => (               // ********
        <div>My Component</div>  // renderer
    );                           // ********
})

Component Scope

The function given to component() will be called only one time creating a closure allowing us to define the local variables and state to be used by the actual renderer. Whatever is defined in that scope will stay and won't be re-created in each renders. So no need to use useMemo or useCallback hooks to prevent things to be re-created. In fact hooks API won't work in this scope

So this is the place we define the local component state, callbacks or any variables to be used by the renderer, as well as life-cycle hooks

Renderer

The renderer is the function that uses the reactive values or props to produce the markup (virtual node as JSX) for the component. This function is used as a computed value by the reactivity system which means any update of the reactive data which is accessed in this scope will cause a render. The render result is cached and won't be calculated again unless any dependent reactive value is changed.

Defining state

State is defined by using reactive or ref which accepts any object in any shape. When state is mutated any component using that state will get rendered.

import { component, reactive } from '@re-active/react';

const Button = component(() => {
    const state = reactive({
        clicks: 0
    });

    return () => (               
        <button onClick={() => state.clicks++}>
            Clicks: {state.clicks}
        </button> 
    );                           
})

Reactive values can be defined in Component scope as well as outside the component to be shared by multiple components easily.

import { component, ref} from '@re-active/react';

const clicks = ref(0);

const Button = component(() => {
    return () => (               
        <button onClick={() => state.clicks++}>
            Clicks: {clicks.value}
        </button>
    );                           
})

const Container = component(() => {
    return () => (
        <div>
            <Button/>
            {clicks.value}
        </div>
    )
})

Reacting to props

Props are also reactive so that they can be referenced in computed values (such as renderer function), they can be watched and can be used in effect

When does a reactive component render?

Under the hood the renderer function of a reactive component is called in an effect. As any other effect callback it is called only if some referenced reactive data is updated. In other words, unreferenced reactive data update won't cause any render.

As long as the mutation of state are sync, no matter how many state variables are changed there will be only one render after the mutations.

const Component = component(() => {
  const state = reactive({
    clicks: 0,
    unusedValue: 0
  });
  
  // Causes only one render even though state is updated multiple times
  function incrementTwice() {  
    state.clicks++;
    state.clicks++;
  }

  return () => {
    return (
      <div>
        <button onClick={incrementTwice}>{state.clicks}</button>
        <button
          onClick={() => {
            // unusedValue is not referenced during render
            // which means it does not affect the rendered result
            // that's why updating this variable does not cause render
            state.unusedValue++;
          }}
        >
          Update unreferenced reactive variable
        </button>
      </div>
    );
  };
});

In the example above, the unusedValue may seem to be used in the renderer at first look and the component may be expected to re-render as a result of updating this variable but this is not correct. Actually it's not used in the render and it does not affect the rendered content. It's referenced in the callback and the callback function is not part of the rendered markup. Reactive components are smart to only render if a reactive value is referenced in the resulting markup.

Lifecycles and Handle / Ref

Since we leverage the whole reactivity system we no longer need react hooks. No useState is needed since state is managed with reactive values. No useMemo, useRef or useCallback because whatever we define in the component scope will persist. And also no dependency arrays needed since there is no stale state scenario. Here are the list of hooks can be called in the component local scope.

onMounted() / onUnmounted()

Both accepts a callback to be called when the component is mounted or unmounted. onMounted also accepts a function returning a cleanup function which will be called when the component is unmount. This can be useful to cleanup things in the same scope.

import { component, ref, onMounted, onUnmounted } from "@re-active/react";

export const Timer = component((props) => {
    let timer;

    const seconds = ref(0);

    onMounted(() => {
        timer = setInterval(() => {
            seconds.value++;
        }, 1000);
    });

    onUnmounted(() => {
        clearInterval(timer);
    });

    return () => <div>{seconds.value} seconds passed</div>;
});

Or with cleanup function

import { component, ref, onMounted } from "@re-active/react";

export const Timer = component((props) => {
    const seconds = ref(0);

    onMounted(() => {
        const timer = setInterval(() => {
            seconds.value++;
        }, 1000);
        
        // Will be called when component unmounts.
        return () => {
            clearInterval(timer);
        }
    });

    return () => <div>{seconds.value} seconds passed</div>;
});

This lifecycle dependent logic can also be extracted and reused as follows

function timer() {
    const seconds = ref(0);

    onMounted(() => {
        const timer = setInterval(() => {
            seconds.value++;
        }, 1000);
        
        return () => {
            clearInterval(timer);
        }
    });

    return seconds;
}

export const Lifecycle = component(() => {
    const seconds = timer();

    return () => (
        return <div>{seconds.value} seconds passed</div>;
    );
});

imperativeHandle and component.withHandle()

component.withHandle is used to create component with a forwarded ref just like React.forwardRef. You can expose the ref as any object to parent components using imperativeHandle.

// works like React.forwardRef
const Input = component.withHandle<P, H>((props, ref) => {

    // we can keep a reference to input element simple as this
    // React.createRef() can be used as well;
    let input;

    imperativeHandle({
        focus() {
            input.focus();
        }
    });

    return () => <input ref={r => input = r} />;
});

Context Api

React context API works the same way in reactive components. When a context is provided it can be consumed using consumeContext from the component local scope.

import { component, reactive, consumeContext } from "@re-active/react";

const Context = React.createContext();

const Label = component((props) => {

  // consumeContext returns the context as 'ref'
  const context = consumeContext(Context);
  
  return () => <label>{context.value.text}</label>;
});

export const Component = component((props) => {

  const contextValue = reactive({ text: "" });

  return () => (
    <div>
      <input
        value={contextValue.text}
        onChange={(e) => (contextValue.text = e.target.value)}
      />
      <Context.Provider value={contextValue}>
        <Label />
      </Context.Provider>
    </div>
  );
});

Defining a Functional Component

This is the other overload of component() which accepts an ordinary 'react functional component' and acts like a HOC and returns a new component that is bound to the reactive data.

It's already ref forwarded so you don't need to use forwardRef.

import { component, reactive } from '@re-active/react';

const clicks = reactive(0);

export const Foo = component((props, ref) => {

    return (
        <div>
            <div>{clicks.value}</div>
            <button onClick={() => clicks.value++}>increment</button>
        </div>
    )
});

Interoperability with React components

Reactive plays well with ordinary React components, class or functional. At the end of the day component() produces a memoized functional components with hooks. You can still use existing react components in the tree, pass props of callbacks.

If you still feel like to use React hooks Api just make a wrapper component with hooks and use render props to pass necessary data to a reactive component.

Last updated