

What is Xyjax
Xyjax is the React+Redux-based tool for single-page applications development. The main goal of this tool is to give you the opportunity to write code that is clean, readable and easily reusable. But Xyjax is not only about code readability - much attention has been paid to the ease and convenience of optimizing and testing of your code too.
Why Xyjax
- Every action with Redux state can be done with one API call so you don't need Redux actions, dispatches, reducers, etc. anymore, everything works just out of the box!
- Xyjax provides you to control components render with one API call
- Powerful and flexible components' properties validation system
- Simple and powerful events system makes it possible for you to build event-based scenarios of any complexity
- There is nothing in your code refers to React/Redux inner logic so it's possible to write completely clean code which contains nothing but your application logic
- Xyjax is easy to use, pretty good documented and has code examples so it won't be difficult to get into it
How to import Xyjax
To work with Xyjax you should import xjx API object which can be done in following manner after installing this npm package:
import xjx from 'xyjax'
You don't need anything else to work with Xyjax, so it makes possible to have less imports in your code.
Demo, source code examples
Take a look at demo and source code examples for quick overview (under development still).
Documentation
1. Base API methods
This section contains base methods which are located strictly in xjx API object.
1-1. xjx.begin
This method is your application entry point.
Method signature:
xjx.begin(
MainComponent,
elementId = 'root',
xjx_id = 'xjx-app',
initialStateGenerator = null,
reduxExtension = true
)
Arguments description:
- MainComponent is an application entry point React component
- elementId is an application HTML-container id
- xjx_id is a Xyjax component identifier will be applied to MainComponent
- initialStateGenerator must be passed if you need initial state. It must be a function with no arguments, which returns object that will be your application initial state
- reduxExtension is a boolean flag which indicates if application store will be connected to Redux DevTools extension
Basic usage example:
Basic case (when you have an HTML div with 'root' id and a component named App) is very simple in spite of many arguments:
import xjx from 'xyjax'
xjx.begin(App)
There are just three short lines of code. You don't need ReactDOM.render... etc. anymore - everything works fine just out of the box!
Initial state generator usage example:
Basic correct initial state generator:
const correct_initial_state_generator = () => {
return {
state_field: 'field_value'
}
}
You can use following approach in case of having more complex initial state than just a hardcoded values bunch:
const more_complex_initial_state_generator = (variable_field_value) => {
return () => {
return {
state_field: variable_field_value
}
}
}
more_complex_initial_state_generator method will return correct initial state generator.
1-2. xjx.connect
This method connects React components to a Redux state. It must be used at components exporting.
Method signature:
xjx.connect(
component
)
Arguments description:
- component is a current React component that will be connected to store
Usage:
export default xjx.connect(TestComponent)
1-3. xjx.init
This method initializes React component with Xyjax identifier and some other configs if needed. Your components render methods should contain results of this method evaluation upon components, not plain components!
Method signature:
xjx.init(
Component,
id = null,
props = null,
firstRender = true,
userShouldCompUpdate = null
)
Arguments description:
- Component is the current React component being initialized
- id is the string with Xyjax identifier or the function with two parameters (className and props) which returns Xyjax identifier (see examples below). If nothing was passed, Xyjax identifier of current component would be the same as its' class name.
- props is the object will be applied as initialized component's props. Properties are passing as javascript object to make it easy for you to validate it before passing and/or to extract them in constants (objects or functions which return required properties)
- firstRender is the Boolean flag which shows if component's render is allowed or denied when it's mounted
- userShouldCompUpdate is the function that must be passed if you want to ignore Xyjax render control system. It has more priority than Xyjax render permission check logic though this approach is not recommended
Usage:
render() {
const testComp = xjx.init(TestComponent)
const testCompId = 'testComp'
const testCompProps = (helloMessage) => { return { hello: helloMessage } }
const customTestComp = xjx.init(
TestComponent,
testCompId,
testCompProps('world')
)
const anotherTestCompId = (className, props) => className + '-' + props.index
const anotherTestCompProps = { index: 1 }
const anotherTestComp = xjx.init(
TestComponent,
anotherTestCompId,
anotherTestCompProps
)
return (
<div>
{testComp}
{customTestComp}
{anotherTestComp}
</div>
)
}
2. State SubAPI
This section contains methods should be used for updating and obtaining your application state. xyjax-update-immutable and xyjax-required-value NPM packages are widely-used here to implement this functionality.
2-1. xjx.state.to
Provides Redux state updating at any nesting level.
Method signature:
xjx.state.to(
path,
changes,
message
)
Arguments description:
- path is a string with path to target field in state (fields' separator is dot, for example 'rootField.nestedField.oneMoreField')
- changes parameter contains changes that will be aquired by Redux state, changes can be primitive literal (string, number, boolean, etc.), object or calculating field (see example 3).
- message is an optional string parameter which will be added to action type. This is pretty useful if you use Redux DevTools extension. Equals to path parameter by default.
State updating examples:
Example 1. Creating new fields in state
In this example we have empty initial state
xjx.state.to('test_field', 523)
xjx.state.to('test_object.math_random_field', Math.random())
xjx.state.to('test_object.string_field', 'string_field_value')
xjx.state.to('another_test_object', {
field_1: 'hello', field_2: 'world'
})
xjx.state.to('root_field.nested_object.one_more_level', {
first_field: 'first',
second_field: 'second'
})
xjx.state.to('even.more.nested.than.just_nested.field', 15)
Final state will look like this:
const final_state = {
test_field: 523,
test_object: {
math_random_field: 0.4648380646500174,
string_field: 'string_field_value'
},
another_test_object: {
field_1: 'hello',
field_2: 'world'
},
root_field: {
nested_object: {
one_more_level: {
first_field: 'first',
second_field: 'second'
}
}
},
even: {
more: {
nested: {
than: {
just_nested: {
field: 15
}
}
}
}
}
}
Example 2. State parts updating and rewriting
In this example we have following initial state:
const initial_state = {
test_object: {
field_1: 'testfield1',
field_2: 'testfield2',
some_inner_structure: {
some_inner_field: 'innerfieldvalue',
second_inner_field: 'secondinnerfieldvalue'
},
second_inner_structure: {
some_inner_field: 'innerfieldvalue',
second_inner_field: 'secondinnerfieldvalue'
}
},
test_object_2: {
field_1: 'anothertestfield1',
field_2: 'anothertestfield2'
},
test_array: [
{field_1: 'first_field_1', field_2: 'first_field_2'},
{field_1: 'second_field_1', field_2: 'second_field_2'},
{field_1: 'third_field_1', field_2: 'third_field_2'}
]
}
Updating and rewriting approach
These two lines of code can seem to make the same result but they are pretty diffirent in effect:
xjx.state.to('a.b', 'b_value')
xjx.state.to('a', { b: 'b_value' })
First line only updates 'a' field in state, so it changes only 'b' field, other fields in 'a' object stay the same. Second line rewrites 'a' field in state with given object, so previous version of 'a' object will be replaced with { b: 'b_value' } object. Take a look at following examples:
xjx.state.to('test_object.field_1', 'testfield1_modified')
xjx.state.to('test_object_2', { field_1: 'testfield1_modified' })
xjx.state.to(
'test_object.some_inner_structure.some_inner_field',
'modified-some-inner-field'
)
xjx.state.to(
'test_object.second_inner_structure',
{some_inner_field: 'modified-some-inner-field'}
)
xjx.state.to('test_array.0.field_1', 'testfield1_modified')
xjx.state.to('test_array.1', {field_1: 'testfield1_modified'})
Final state:
const final_state = {
test_object: {
field_1: 'testfield1_modified',
field_2: 'testfield2',
some_inner_structure: {
some_inner_field: 'modified-some-inner-field',
second_inner_field: 'secondinnerfieldvalue'
},
second_inner_structure: {
some_inner_field: 'modified-some-inner-field'
}
},
test_object_2: {
field_1: 'testfield1_modified'
},
test_array: {
'0': {
field_1: 'testfield1_modified',
field_2: 'first_field_2'
},
'1': {
field_1: 'testfield1_modified'
},
'2': {
field_1: 'third_field_1',
field_2: 'third_field_2'
}
}
}
Example 3. Calculating fields
It's possible to update state not only with strict values but also with calculating fields. It is a function which takes one argument (updating field value before update or parameter default value if field was not found in state) and returns value that will be placed in state by the given path. This is extremely useful for counters, boolean flag toggles, objects merging, arrays modifying etc.
There is a following initial state in this example:
const initial_state = {
test_field: 15,
test_object: {
flag: false,
counter: 5
},
test_array: []
}
Take a look at calculating fields approach:
const muliplyByTwo = (value = 0) => { return value * 2 }
const divideByTen = (value = 0) => { return value / 10 }
const increment = (value = 0) => { return value + 1 }
const toggleBooleanFlag = (currentFlag) => { return !currentFlag }
xjx.state.to('test_field', muliplyByTwo)
xjx.state.to('test_field', divideByTen)
xjx.state.to('new_test_field', increment)
xjx.state.to('new_test_field', increment)
xjx.state.to('test_object.flag', toggleBooleanFlag)
const addElementToArray = (element) => {
return (array = []) => { return array.concat(element) }
}
const elementShouldBeAdded = {hello: 'world'}
xjx.state.to('test_array', addElementToArray(elementShouldBeAdded))
We have following state as a result of these evaluations:
const final_state = {
test_field: 3,
test_object: {
flag: true,
counter: 5
},
new_test_field: 2,
test_array: {
'0': {
hello: 'world'
}
}
}
2-2. xjx.state.to_sync
This method has same functionality as xjx.state.to and additional possibility of having function which will evaluate only after state updating will finish (Redux state update is asynchronious).
Method signature:
xjx.state.to_sync(
path,
changes,
syncHandler,
message
)
Arguments description:
- path is a string with path to target field in state (fields' separator is dot, for example 'rootField.nestedField.oneMoreField')
- changes parameter contains changes that will be aquired by Redux state, changes can be primitive literal (string, number, boolean, etc.), object or calculating field (see example 3 in xjx.state.to method description)
- syncHandler is a function with no arguments which evaluates syncroniously after state update finishing
- message is an optional string parameter which will be added to action type. This is pretty useful if you use Redux DevTools extension. Equals to path parameter by default.
Usage example:
const increment = (x = 0) => { return x + 1 }
xjx.state.to('not_sync_counter', increment)
console.log(xjx.state.from('not_sync_counter'))
const showSyncCounter = () => { console.log(xjx.state.from('sync_counter')) }
xjx.state.to_sync('sync_counter', increment, showSyncCounter)
2-3. xjx.state.from
Obtains Redux state data at any nesting level.
Method signature:
xjx.state.from(
path,
defaultValue
)
Arguments description:
- path is the string with path to target field in state with dot as separator (for example 'rootField.nestedField.oneMoreField')
- defaultValue parameter contains value that will be returned if nothing would be found in state by given path or function which takes found value and returns value that will be used in your application which makes it possible for you to use any validations, mappings or data checks when you obtain data from Redux state
Usage example:
In following example we have following initial state:
const initial_state = {
field1: 'value1',
field2: {
field2_1: 'value2_1',
field2_2: {
nested_data: 'hello'
}
},
field3: 5
}
const temp1 = xjx.state.from('field1')
const temp2 = xjx.state.from('field2')
const temp3 = xjx.state.from('field2.field2_2.nested_data')
const temp4 = xjx.state.from('not.existing.field', 15)
const temp5 = xjx.state.from('field3', (x) => { return (x % 2) ? 'odd' : 'even' })
3. Render SubAPI
This section contains methods which are used for React components' render control in your application.
3-1. xjx.render.allow_once
Allows component with given Xyjax component identifier rendering after one following state update.
Method signature:
xjx.render.allow_once(
component_xjx_id
)
Arguments description:
- component_xjx_id is a string with Xyjax component identifier that is allowed be rendered after next following state update
3-2. xjx.render.allow
Allows component with given Xyjax component identifier rendering until its' render denying.
Method signature:
xjx.render.allow(
component_xjx_id
)
Arguments description:
- component_xjx_id is the string with Xyjax component identifier which render must be allowed
3-3. xjx.render.deny
Denies rendering of a component with given Xyjax component identifier (can be allowed later).
Method signature:
xjx.render.deny(
component_xjx_id
)
Arguments description:
- component_xjx_id is the string with Xyjax component identifier which render must be denied
4. Events SubAPI
This section contains methods which are used in case of making event-based scenarios in your application. This functionality is powered by xyjax-events NPM package.
4-1. xjx.events.on_toState
Provides handlers adding on state updating.
Method signature:
xjx.events.on_toState(
key,
handler
)
Arguments description:
- key is a state part which will be observing. There are three types of keys:
- strict key (e.g. 'a.b') - exactly given path in state will be observed
- left side varied key (e.g. '...a.b') - every path which ends with given data (including given data itself) will be observed (e.g. 'a.b', 'root.a.b', 'root.another_root.a.b')
- right side varied key (e.g. 'a.b...') - every path which starts with given data (including given data itself) will be observed (e.g. 'a.b', 'a.b.nested', 'a.b.nested.another_nested')
- both side varied key (e.g. '...a.b...') - every path which includes given data (including given data itself) will be observed (e.g. 'a.b.', 'root.a.b.nested', 'root.a.b', 'a.b.nested')
- handler is a function that will be called every time target part of state updates. Takes one parameter containing on_toState event arguments which is object of two fields: path and changes
Usage example:
const toState_testHandler = (eventArgs) => {
const { changes, path } = eventArgs
console.log('updated with ' + JSON.stringify(changes) + ' at path ' + path)
xjx.state.to('events.fired_counter', (x = 0) => { return x + 1 })
}
xjx.events.on_toState('root.nested', toState_testHandler)
xjx.events.on_toState('a.b...', toState_testHandler)
xjx.events.on_toState('...c.d', toState_testHandler)
xjx.events.on_toState('...e.f...', tostate_testHandler)
xjx.state.to('root.nested', 'hello')
xjx.state.to('a.b.nested.one.more', Math.random())
xjx.state.to('root.one_more_root.c.d', {test_field: 'test_field_value'})
xjx.state.to('a1.b1.e.f.g', Math.random())
Console output:
updated with "hello" at path root.nested
updated with 0.7842862279990666 at path a.b.nested.one.more
updated with {"test_field":"test_field_value"} at path root.one_more_root.c.d
updated with 0.1234366748018656 at path a1.b1.e.f.g
Also, events.fired_counter field in state equals 3.
4-2. xjx.events.on_fromState
Provides handlers adding on state parts obtaining.
Method signature:
xjx.events.on_fromState(
key,
handler
)
Arguments description:
- key is a state part will be observing, following same system as in xjx.state.on_toState
- handler is a function that will be called every time target part of state being obtained. Takes one parameter containing on_fromState event arguments which is object of two fields: path and value_from_state
Usage example:
There is a following initial state in this example:
const initial_state = {
test_object: {
test_value: 1
},
another_test_object: {
another_test_value: 5
},
root_field: 'a',
w: {
x: {
y: {
z: 7
}
}
}
}
const fromState_testHandler = (eventArgs) => {
const { path, value_from_state } = eventArgs
console.log('obtained ' + JSON.stringify(value_from_state) + ' from ' + path)
}
xjx.events.on_fromState('root_field', fromState_testHandler)
xjx.events.on_fromState('test_object...', fromState_testHandler)
xjx.events.on_fromState('...another_test_value', fromState_testHandler)
xjx.events.on_fromState('...x.y...', fromState_testHandler)
xjx.state.from('root_field')
xjx.state.from('test_object.test_value')
xjx.state.from('another_test_object.another_test_value')
xjx.state.from('w.x.y.z')
Which makes following console output:
obtained "a" from root_field
obtained 1 from test_object.test_value
obtained 5 from another_test_object.another_test_value
obtained 7 from w.x.y.z
5. React components
All your React components in Xyjax application should be inherited from any component from this section and not from React.Component.
5-1. xjx.base_component
It's a basic Xyjax component, you can control its' render with xjx.render API. You should extend it in following manner (same as React.Component):
class TestComponent extends xjx.base_component {
Methods
Returns Xyjax identifier of the current component. You should call it like this.id() in any your component which is extended from xjx.base_component.
There is a TestComponent component which is extended from xjx.base_component in following example:
render() {
const identifier = this.id()
return (
<div>{identifier}</div>
)
}
const testComponentWithCustomId = xjx.init(TestComponent, 'custom-id')
const testComponent = xjx.init(TestComponent)
testComponentWithCustomId component render result:
custom-id
testComponent component render result:
TestComponent
Static fields
Will be used as props validation config if exists. Object validation documentation and examples.
Take a look at propsValidator declaration example for TestComponent:
class TestComponent extends xjx.base_component {
static propsValidator = {
fieldsList: {
must: ['a']
},
fieldsValues: {
a: {
must:
[
{title: 'mustBeNumber', check: (x) => typeof x == 'number'}
]
}
}
}
...
There can be some problems with className property in component id generation when initialized ('p' as className when bundle is in production mode e.g.) so you can manually set value will be used as className when generating ids. Take a look at example:
class TestComponent extends xjx.base_component {
static componentName = 'custom_test_component_name'
...
const customComponent = xjx.init(
TestComponent,
(className, props) => className + '_' + props.index,
{ index: 5 }
)
customComponent.id()