Dialect fACTORY
Dactory is a library that allows you to use React's transpiler for different purpose. React is a view layer. It takes care for the rendering part and the actual DOM. Dactory is opposite. It is dealing with the business logic of our applications. It allows us to write markup and basically create our own dialect based on the JSX syntax.
/** @jsx D */
import { D, speak } from 'dactory';
const Greeting = function({ name }) {
console.log(`Hello dear ${name}!`);
};
const Text = function({ what }) {
console.log(`You know what ... ${what}!`);
};
speak(
<D>
<Greeting name="Jon Snow" />
<Text what="winter is coming" />
</D>
);
/* Outputs:
Hello dear Jon Snow!
You know what ... winter is coming.
*/
You must add the @jsx
comment at the top of your files. And you must import D
function. Otherwise Dactory will not work.
Grab the library by running npm install dactory
or yarn install dactory
. Dactory uses JSX as a base so you have to have some sort of Babel transpilation setup. Check out the examples folder to get an idea how to do it.
The code that we write follows the JSX syntax. You don't have to learn anything new. If you ever worked with JSX you already know how to write code that Dactory understands.
The core API of Dactory is just two functions - D
and speak
. Every tag that we write gets transpiled to D()
calls similarly to React.createElement
. The more interesting one is speak
. It accepts a markup-like code which we will define as dialect and every tag inside as a word. The dialect describes in a declarative fashion what our program does.
The order of the execution is from top to bottom and from outer to inner words.
const Foo = () => console.log('Foo');
const Bar = () => console.log('Bar');
const Mar = () => console.log('Mar');
speak(
<Foo>
<Bar />
<Mar />
</Foo>
);
/* Outputs:
Foo
Bar
Mar
*/
The speak
function is asynchronous. Dactory makes an assumption that all the words in our dialect are also asynchronous. For example:
const Fetch = async function ({ url }) {
return await fetch(url);
}
const App = function () {}
await speak(
<App>
<Fetch url="https://jsonplaceholder.typicode.com/posts" />
<Fetch url="https://jsonplaceholder.typicode.com/users" />
</App>
);
If there are multiple asynchronous functions they are executed one after each other. If you need to run something in parallel keep reading. There's a word in the predefined words section for that.
Every dialect gets executed with a given context. The context is just a plain JavaScript object and all the words in the dialect has access to it. In fact the speak
function accepts one as a second argument (by default set to {}
). We also receive the context when the promise returned by speak
is resolved. Which means that if we want to get something back we have to inject it into the context because that's the only one output of the speak
's call. This happens by using the special exports
prop like so:
const GetAnswer = async function () {
// this gets assigned to `answer` prop in the context
return 42;
};
speak(<GetAnswer exports="answer" />)
.then(context => {
console.log(context.answer); // 42
});
Think about exports
as something that defines a property in context. The value of that newly defined property is what our word returns.
Passing data between words happens by adding a prop with no value and same name prefixed with $
. For example:
const GetAnswer = async function() {
return 42;
};
const Print = function({ answer }) {
console.log(`The answer is ${answer}.`);
};
const App = function() {};
speak(
<App>
<GetAnswer exports="answer" />
<Print $answer />
</App>
);
GetAnswer
defines a property answer
in our context which becomes { answer: 42 }
. Later Print
says "I need answer
prop from the context.
That's not the only one way to pass data around. The function as children pattern works here too:
function GetTitle() {
return 'developer';
}
function PrintUser({ title, name }) {
console.log(`Hello ${name} ${title}!`);
}
function App() {
return 'Boobooo';
}
speak(
<App exports="name">
<GetTitle>{ title => <PrintUser title={title} $name /> }</GetTitle>
</App>
);
PrintUser
receives two props title
and name
. title
comes from what GetTitle
returns while name
comes from the context.
It's just easier to write it as markup:
speak(
<App exports="name">
<GetTitle exports="title">
<PrintUser $title $name />
</GetTitle>
</App>
);
If we for some reason don't like the naming in our context we may change it by adding a value to the prefixed prop. For example:
speak(
<App exports="name">
<GetTitle exports="title">
<PrintUser $title $name='applicationName' />
</GetTitle>
</App>
);
PrintUser
will receive { title: '...', applicationName: '...' }
instead of { title: '...', name: '...' }
.
Because speak
returns a promise we can just catch
the error at a global level:
const Problem = function() {
return iDontExist; // throws an error "iDontExist is not defined"
};
const App = function() {};
speak(
<App>
<Problem />
</App>
).catch(error => {
console.log('Ops, an error: ', error.message);
// Ops, an error: iDontExist is not defined
});
That's all fine but it is not really flexible. What we may want is to handle the error inside our dialect. In such cases we have the special onError
prop. It accepts another dialect which receives the error as a prop.
const Problem = function() {
return iDontExist;
};
const App = function() {};
const HandleError = ({ error }) => console.log(error.message); // logs "iDontExist is not defined"
speak(
<App>
<Problem onError={ <HandleError /> } />
</App>
);
Dactory has several strategies for handling errors:
- If there's no handler provided the error bubbles up
- If there's a handler and the handler returns
false
the error is swallowed. Dactory stops the execution of the current set of words. - If there's a handler and the handler returns
true
the error is swallowed. Dactory continues the execution of the current set of words. - If there's a handler and the handler returns nothing the error bubbles up.
By stopping the current set of words we meant:
const Problem = function() {
return iDontExist;
};
const App = function() {};
const Wrapper = function() {};
const HandleError = () => {};
const A = () => console.log('A');
const B = () => console.log('B');
const C = () => console.log('C');
await speak(
<App exports='answer'>
<Wrapper>
<Problem onError={ <HandleError /> } />
<A />
</Wrapper>
<Wrapper>
<B />
<C />
</Wrapper>
</App>
);
We will see B
followed by C
but not A
because there's an error at that level.
Obviously we don't have a straight business logic. It has branches. Dactory has no API for this. The cheapest solution for that is the function as children pattern:
function MyLogic({ answer }) {
if (answer === 42) {
return true;
}
return false;
}
function PrintCorrectAnswer() {
console.log('Correct!');
}
function PrintWrongAnswer() {
console.log('Wrong!');
}
function App() {}
await speak(
<App>
<MyLogic answer={ 42 }>
{
isCorrect => isCorrect ? <PrintCorrectAnswer /> : <PrintWrongAnswer />
}
</MyLogic>
</App>
);
There are options that define the behavior of Dactory while processing your word. For example if you want to run your word's children in parallel you can to use the processChildrenInParallel
option.
function A() {
return new Promise((done) => {
setTimeout(() => {
console.log('A');
done();
}, 30);
})
}
function B() {
console.log('B');
}
function C() {}
C.processChildrenInParallel = true;
speak(
<C>
<A />
<B />
</C>
);
Without C.processChildrenInParallel = true
we will get A
followed by B
. That's because Dactory will wait till A finishes to run B
. However, processChildrenInParallel
makes A
and B
run in parallel and we are getting B
followed by A
.
Dactory comes with some predefined words.
So far in the examples above we had to define a wrapper function like function App() {}
. Instead we can simply use <D />
. For example:
/** @jsx D */
import { D, speak } from 'dactory';
const Foo = function () {
console.log(`Hello world!`);
}
speak(<D><Foo /></D>);
/** @jsx D */
import { D, speak, Parallel } from 'dactory';
const A = async function() {}
const B = async function() {}
speak(<Parallel><A /><B /></Parallel>);
Dactory runs B
without waiting for A
to be resolved.