Integration with React
In this guide, we'll build a simple React app to interact and manipulate the state of a Reka instance along with a renderer for Reka components.
Installation
We'll be using next
to scaffold a React application. We'll also be using some React-specific APIs for Reka provided by the @rekajs/react
package.
npm install next @rekajs/core @rekajs/types @rekajs/react
The
@rekajs/react
package is still a work in progress is subject to change and improvements
Basic setup
First, we'll create a Reka instance and load it with some components.
We will expose this Reka instance so we can access it throughout our React application with the RekaProvider
context provider.
Lastly, we will create a new <Editor />
component where we will provide some UI to interact with the Reka instance along with a <Preview />
component that will render the components in our Reka instance:
tsx
// src/pages/index.tsximport { Reka } from '@rekajs/core';import { RekaProvider } from '@rekajs/react';import * as t from '@rekajs/types';import * as React from 'react';import { Editor } from '@/components/Editor';import { Preview } from '@/components/Preview';const reka = Reka.create();reka.load(t.state({extensions: {},program: t.program({globals: [t.val({name: 'globalText',init: t.literal({ value: 'Global Text!' }),}),],components: [t.rekaComponent({name: 'App',props: [],state: [],template: t.tagTemplate({tag: 'div',props: {className: t.literal({value: 'bg-neutral-100 px-3 py-4 w-full h-full',}),},children: [t.tagTemplate({tag: 'h4',props: {className: t.literal({ value: 'text-lg w-full' }),},children: [t.tagTemplate({tag: 'text',props: {value: t.literal({ value: 'Hello World' }),},children: [],}),],}),t.componentTemplate({component: t.identifier({ name: 'Button' }),props: {},children: [],}),],}),}),t.rekaComponent({name: 'Button',props: [t.componentProp({name: 'text',init: t.literal({ value: 'Click me!' }),}),],state: [t.val({ name: 'counter', init: t.literal({ value: 0 }) })],template: t.tagTemplate({tag: 'button',props: {className: t.literal({ value: 'rounded border-2 px-3 py-2' }),onClick: t.func({params: [],body: t.block({statements: [t.assignment({left: t.identifier({ name: 'counter' }),operator: '+=',right: t.literal({ value: 1 }),}),],}),}),},children: [t.tagTemplate({tag: 'text',props: {value: t.identifier({ name: 'text' }),},children: [],}),t.tagTemplate({tag: 'text',props: {value: t.binaryExpression({left: t.literal({ value: ' -> ' }),operator: '+',right: t.identifier({ name: 'counter' }),}),},children: [],}),],}),}),],}),}));export default function Home() {return (<RekaProvider reka={reka}><div className="flex h-screen"><div className="w-3/6 h-full border-r-2"><Editor /></div><div className="flex-1"><Preview /></div></div></RekaProvider>);}
Preview
Let's first start building the <Preview />
component, which will essentially contain our renderer for the components we have in our Reka instance.
In order to render a RekaComponent
, we need to first have a Frame
, which is essentially an instance of the component that computes a View
, the render tree of that component's instance.
To keep this guide simple, we will just create some frames manually right after initializing our Reka instance:
tsx
// src/pages/index.tsximport { Reka } from '@rekajs/core';import { RekaProvider } from '@rekajs/react';import * as t from '@rekajs/types';import * as React from 'react';import { Editor } from '@/components/Editor';import { Preview } from '@/components/Preview';const reka = Reka.create();reka.load(...);reka.createFrame({id: 'Main app',component: {name: 'App',},});reka.createFrame({id: 'Primary button',component: {name: 'Button',props: {text: t.literal({ value: 'Primary button' }),},},});export default function Home() {...}
Now, let's actually create the <Preview />
React component for our frames:
tsx
// src/pages/Preview.tsximport { Frame } from '@rekajs/core';import { observer, useReka } from '@rekajs/react';import * as React from 'react';import { RenderFrame } from '../Renderer';export const Preview = observer(() => {const { reka } = useReka();const [selectedFrame, setSelectedFrame] = React.useState<Frame>(reka.frames[0]);return (<div className="w-full h-full flex flex-col text-xs"><div className="px-2 py-2 border-b-2"><selectonChange={(e) => {const frameId = e.target.value;const frame = reka.frames.find((frame) => frame.id === frameId);if (!frame) {return;}setSelectedFrame(frame);}}>{reka.frames.map((frame) => (<option key={frame.id} value={frame.id}>{frame.id}</option>))}</select></div><div className="flex-1 px-2 py-2">{selectedFrame ? (<RenderFrame frame={selectedFrame} />) : (<div className="px-3 py-4">No frame selected</div>)}</div></div>);});
Reka's State is built with Mobx. The
observer
HOC used above is a re-export of the same HOC from themobx-react-lite
package.
Renderer
Next, let's create a Renderer for a View
from a given Frame
:
All we've to do here is go through each type of View
, and return a corresponding React element:
tsx
// src/components/Renderer.tsximport { observer } from '@rekajs/react';import { Frame } from '@rekajs/core';import * as t from '@rekajs/types';import * as React from 'react';type RendererProps = {view: t.View;};export const Renderer = observer((props: RendererProps) => {if (props.view instanceof t.TagView) {if (props.view.tag === 'text') {return <span>{props.view.props.value}</span>;}return React.createElement(props.view.tag,props.view.props,props.view.children.map((child) => (<Renderer key={child.id} view={child} />)));}if (props.view instanceof t.RekaComponentView) {return props.view.render.map((r) => <Renderer key={r.id} view={r} />);}if (props.view instanceof t.ExternalComponentView) {return props.view.component.render(props.view.props);}if (props.view instanceof t.SlotView || props.view instanceof t.FragmentView) {return props.view.children.map((r) => <Renderer key={r.id} view={r} />);}if (props.view instanceof t.ErrorSystemView) {return (<div className="">Something went wrong. <br />{props.view.error}</div>);}return null;});type RenderFrameProps = {frame: Frame;};export const RenderFrame = observer((props: RenderFrameProps) => {if (!props.frame.view) {return null;}return <Renderer view={props.frame.view} />;});
Editor
Now, let's create a simple Editor component that interacts with our Reka
instance. We'll simply add a button that adds a new text template to our App RekaComponent
:
tsx
// src/components/Editor.tsximport * as t from '@rekajs/types';import { useReka } from '@rekajs/react';import * as React from 'react';export const Editor = () => {const { reka } = useReka();return (<div className="w-full h-full p-4"><buttonclassName="text-sm px-3 py-2 rounded bg-neutral-200 text-neutral-600"onClick={() => {const appComponent = reka.state.program.components.find((component) => component.name === 'App');if (!appComponent) {return;}reka.change(() => {appComponent.template.children.push(t.tagTemplate({tag: 'text',props: {value: t.literal({ value: "I'm a new text template!" }),},children: [],}));});}}>Add a new text template</button></div>);};
Parser
Let's edit our previous example to create the text template with a value from text input.
Since the text value is a prop of a template, and template props are expressions, we will need to first add the @rekajs/parser
package to our project so we can parse user inputs into an expression AST Node.
npm install @rekajs/parser
Now let's go back and update our Editor
component to include an input field with the Reka parser:
tsx
...import { Parser } from '@rekajs/parser';export const Editor = () => {...const [newTextValue, setNewTextValue] = React.useState('');return (<div><inputtype="text"placeholder="New value"value={newTextValue}onChange={(e) => setNewTextValue(e.target.value)}/><buttonclassName="text-sm px-3 py-2 rounded bg-neutral-200 text-neutral-600"onClick={() => {if (!newTextValue) {return;}try {const parsedTextValue = Parser.parseExpression(newTextValue);const appComponent = reka.state.program.components.find((component) => component.name === 'App');if (!appComponent) {return;}reka.change(() => {appComponent.template.children.push(t.tagTemplate({tag: 'text',props: {value: parsedTextValue,},children: [],}));});} catch (err) {console.warn(err);}}}>Add a new text template</button></div>)}
Hence, to create a proper page editor with Reka, all we need to do is to create UI elements that mutate the Reka state as we did above.
Of course, we could probably spend hours in this guide if we were to go through building every single UI element to edit the Reka state. Instead, we will end this guide by replacing the basic UI elements we made above with a code editor:
Let's add one more package:
npm install @rekajs/react-code-editor
Finally, let's update our Editor component:
tsx
import { CodeEditor } from '@rekajs/react-code-editor';import * as React from 'react';export const Editor = () => {return (<div className="w-full h-full p-4"><CodeEditor /></div>);};