spaceace-legacy
v0.24.2
Published
A fancy immutable storage library for JavaScript
Downloads
2
Readme
SpaceAce
A fancy immutable storage library for JavaScript
Intro
SpaceAce is used to store the state of your front-end application. Think of it as a replacement for this.setState(…)
in your React apps.
The goal is a library that is as powerful and useful as Redux, but more modular with an easier to use API.
Any and all feedback is welcome!
Benefits
- Immutable – Centralized state with easy to track changes
- Modular – Easily create sub-states for each component
- No Boilerplate – Components can merge changes onto their part of the state themselves, no need to declare actions or have have a central reducer.
- Convenient API – Create sub-spaces and bind to events within your render functions.
Example Usage
SpaceAce can be used with any front-end view library (such as Vue and Angular), but the examples below are done with React.
index.js
import react from 'react';
import ReactDOM from 'react-dom';
import Space from 'spaceace';
import Container from './Container';
// Create the root "space" along with its initial state
const space = new Space({ name: 'Jon', todos: [] });
space.subscribe(causedBy => {
// Example `causedBy`s:
// 'todoList#addTodo', 'todos[akd4a1plj]#toggleDone'
console.log(`Re-render of <Container /> caused by ${causedBy}`);
ReactDOM.render(
<Container space={space} />,
document.getElementById('react-container')
);
});
Container.js
import react from 'react';
import TodoList from './TodoList';
export default function Container({ space }) {
const state = space.state;
return (
<div>
<h1>Welcome {state.name}</h1>
<TodoList
// subSpace takes over `state.todos`, turning it into a child space
space={space.subSpace('todos')}
name={state.name}
/>
</div>
);
TodoList.js
import react from 'react';
import uuid from 'uuid/v4';
import Todo from 'Todo';
export default function TodoList({ space, name }) {
const todos = space.state;
return(
<h2>{name}'s Todos:</h2>
<button onClick={space.bindTo(addTodo)}>Add Todo</button>
<ul className='todos'>
{todos.map(todo =>
<Todo space={space.subSpace(todo.id)} />
)}
</ul>
);
};
// bindTo callbacks are given the space first, then the event.
// If the space is an array, the result overwrites the existing state
function addTodo(space, e) {
const todos = space.state;
e.preventDefault();
return [
// All items that exist in a list, like this one, need a unique 'id'
// for when they are later accessed as a subSpace
// uuid() is a handy module for generating a unique 'id'
{ id: uuid(), msg: 'A new TODO', done: false }
].concat(todos)
};
}
}
Todo.js
import react from 'react';
export default function Todo({ space }) {
const todo = space.state; // The entire state from this space is the todo
const { bindTo } = space;
const doneClassName = todo.done ? 'done' : '';
return (
<li className="todo">
<input type="checkbox" checked={done} onChange={bindTo(toggleDone)} />
<span className={doneClassName}>{todo.msg}</span>
<button onClick={bindTo(removeTodo)}>Remove Todo</button>
</li>
);
}
// The returned value from an action is merged onto the existing state
// In this case, only the `done` attribute is changed on the todo
function toggleDone({ state: todo }, e) {
return { done: !todo.done };
}
// Returning null causes the space to be removed from its parent
function removeTodo(todoSpace, e) {
e.preventDefault();
return null;
}
Browser Compatibility
lib/Space.js
targets ES5. It makes use of Array.isArray
, Object.freeze
, and Object.defineProperties
. All major browsers from at least the past 5 years should be supported. If support for ancient browsers is needed, check out es5-shim.
Also, Object.assign
is used, which is from ES6, which supports most browsers of the past couple years. You can use es6-shim or just object.assign to add support for older browsers.
If you notice any other browser compatibility issues, please open an issue.
Documentation
What is a Space?
Space
is the default class provided by the spaceace
npm package.
Every instance of Space
consists of:
state -> Object
: An immutable state, which can only be overwritten using an action.subSpace(subSpaceName: String) -> Space
: Spawns a child space.bindTo(eventHandler: Function) -> Function
: Wraps an event handler, passing in the space when eventually called. If object returned by eventHandler, it is recursively merged onto the space's state.applyValue(keyName: String) -> Function
: Given a key name, returns a function that takes a value and sets applies it to the key on the state.setState(mergeObject: Object, [changedBy: String])
: A method for changing a space's state by doing a recursive merge.replaceState(newState: Object, [changedBy: String])
: A method for replacing a space's state.setDefaultState(mergeObject: Object)
: Sets attributes that do not exist yet on the state. Useful for initializing a sub-space's state.subscribe(subscriber: Function) -> Function
: Adds a callback for when the state changes.rootSpace -> Space
: Shortcut to access the top-most ancestor space.
new Space(initialState: Object, [options: Object])
Returns a new space with its state defined by the passed in initialState
.
Optionally pass in an object as a second parameter to change the default behaviour of the space, and any of its sub-spaces.
e.g.
const rootSpace = new Space(
{ initialState: true, todos: [] },
{ skipInitialNotification: true }
);
Options
skipInitialNotification
- Boolean – Default: false – If true, does not invoke subscribers immediately when they're added.
state
state
is a getter method on every space. It returns a frozen/immutable object
that can be used to render a view. It includes the state of any child spaces as well. Do not change it directly.
subSpace(name: String)
Parameters:
- String (the name of the key to spawn from)
Returns:
- A new space linked to the current space
Use subSpace
to take part of the current space's state and return a child space based on it.
When a child space's state is updated, it notifies its parent space, which causes it to update its state (which includes the child's state) and notifies its subscribers, and then notifies its parent space, and so on.
subSpace
is usually called in your component's render for convenience. It's safe to do so because it does not alter state when called.
If not existing attribute exists for a subSpace
to attach to, an empty object will be added and used. Note that even though this causes the parent state to change, no notification is made.
e.g.
const TodoApp = ({ space }) => (
<div>
{space
.subSpace('todos')
.state.map(todo => (
<Todo {...space.subSpace('todos').subSpace(todo.id)} />
))}
</div>
);
or more "properly":
const TodoApp = ({ space }) => (
<div>
<TodoList space={space.subSpace('todos')} />
</div>
);
const TodoList = ({ space: { state: todos, subSpace } }) => (
<ul>
{todos.map(todo => (
<Todo space={subSpace(todo.id)} key={todo.id} />
))}
</ul>);
);
bindTo
Parameters:
- Function (callback)
- Pass-through params (optional)
Returns:
- Function (the callback bounded to the space)
Wraps the given callback function and returns a new function. When the returned function is called, it calls the given callback, but with the space passed in as the first parameter. Any extra parameters given to bindTo
are given to the callback when its called.
This new function can then be used as an event handler.
The event is passed in as the last parameter to the callback, useful for calling event.preventDefault()
, or for reading event.target.value
.
The value returned by your callback will be recursively merged onto the space's state. If the space's state is an array, the returned value will overwrite the state instead.
If the space is an item in a list, then returning null
will remove it from the list.
If a promise is returned, SpaceAce will wait for it to resolve and then recursively merge the results onto the state (or replace if the state is an array).
SpaceAce supports generators, allowing you to return values multiple times by calling yield
with a value that will be recursively merged onto the space's state. With generators, subscribers won't be notified until your function finally calls return
.
e.g.
const changeTodo = (space, event) => ({
content: event.target.value,
status: 'modified'
});
const toggleDone = ({ state: todo }) => ({
done: !todo.done,
status: 'modified'
});
const removeTodo = () => null;
const saveTodo = function* (space, renderedAt, event) => {
event.preventDefault();
console.log(
"Saving todo. It was last rendered at: ",
renderedAt.toISOString()
);
yield { status: 'saving' };
return fetch(…).then(() => ({ status: 'saved' }))
.catch(e => ({ status: 'errorSaving' }));
};
export default Todo = ({ state: todo, bindTo }) => (
<div>
<form onSubmit={bindTo(saveTodo, new Date())}>
<input type="text" value={state.content} onChange={bindTo(changeTodo)} />
<button onClick={bindTo(toggleDone)} type="button">
{todo.done ? 'Restore' : 'Done'}
</a>
<button onClick={bindTo(removeTodo)} type="button">
Remove
</button>
<button type="submit">
Save to server
</button>
</form>
</div>
);
applyValue
Parameters:
- String (key name)
Returns:
- Function (which takes a single parameter: value)
Useful for events that need to change the state.
Given the name of a key on the state, returns a function that assigns any given value to that key.
If the given value is an event (i.e. has a target
with a value
), the event's value is applied instead. If the event's type is checkbox
, then the target's checked
value is applied. If the event's type is number
, the value is converted to a number before being applied.
e.g.
// #applyValue when given an event directly
// Useful for event handling built-in HTML elements
<button onClick={applyValue('currentTab')} value="new" />;
// or
<select onChange={applyValue('currentTab')}>
<option value="new">New this week</option>
<option value="archived">Archived</option>
<option value="deleted">Deleted</option>
</select>;
// #applyValue given a value directly
// Useful for custom component event handling
const ChooseNewTab = ({ onClick }) => {
return <button onClick={() => onClick('new')} />;
};
<ChooseNewTab onClick={applyValue('currentTab')} />;
// Using #bindTo instead
// Notice how this is more complicated,
// so not recommended if you can use #applyValue instead
const chooseNew = () => {
return { currentTab: 'new' };
};
<button onClick={bindTo(chooseNew)} />;
setState
Parameters:
- Object (for merging onto state)
- (optional) String – Used as a name for logging
Shallow merges the given object it into the space's state. Pass in a second parameter to give the update a name for the causedBy
passed to subscribers.
If the space's state is an array, setState will throw an error. Use replaceState
instead.
Often used to apply async data to a state. Use bindTo
to apply changes caused by the user.
e.g.
class TodoApp extends React.Component {
async componentDidMount() {
const { space } = this.props;
const fetchResult = await fetch('/api/todos').then(res => res.json());
space.setState({
todos: fetchResult.todos
}, 'todosFetch');
}
render() {
…
}
}
replaceState
Parameters:
- Object or array
- (optional) String – Used as a name for logging
Replaces the space's state with the object or array provided. Pass in a second parameter to give the update a name for the causedBy
passed to subscribers.
Should be used for altering a state that is an array, or for replaying state.
e.g.
const addTodo = ({ state: todos, replaceState }) => {
replaceState([
...todos,
{
content: 'A brand new todo',
done: false,
},
]);
};
const TodoList = ({ state: todos, subSpace, bind }) => (
<ul>
{todos.map(todo => <Todo {...subSpace(todo.id)} />)}
<button onClick={bindTo(addTodo)}>Add Todo</button>
</ul>
);
setDefaultState
Parameter:
- Object
Sets the specified attributes onto the space's state, if those attributes are currently undefined. Used to initialize a default state for a sub-space.
Note: Be sure to use this instead of setState
in your component constructors to prevent you state from being overwritten when using hot-reloading.
e.g.
class Modal extends React.Component {
constructor(props) {
super(props);
props.setDefaultState({
open: false,
step: 1
});
}
render() {
return …;
}
}
subscribe
Registers a callback function to be called whenever the space's state is updated. This includes if a child space's state is updated.
It calls the subscriber with a single parameter: causedBy
, which specifies why
the subscriber was invoked. It's useful for debugging purposes. The format of
causedBy
is spaceName#functionName
.
Note: For convenience, this subscriber is called immediately when it's declared, with causedBy set to 'initialized'
.
e.g.
const userSpace = new Space({ name: 'Jon' });
userSpace.subscribe(causedBy => {
console.log('Re-rendered by: ', causedBy);
ReactDOM.render(
<Component {...userSpace} />,
document.getElementById('react-container')
);
});
rootSpace
Example: space.rootSpace
Returns the top-most level space.
When deep in a sub-space in can be necessary to access the top-level space of the application. For example, you may have a Login
component that needs to add the newly logged-in user's info to the root of the application's space, so that it can be available to other components in the app.
e.g.
const handleSignup = async ({ state, rootSpace }, event) => {
event.preventDefault();
var result = await fetch(…, { body: state }).then(res => res.json());
rootSpace.setState({ user: result.userInfo });
};
const SignupForm = ({ state, bindTo }) => (
<form onSubmit={bindTo(handleSignup)}>
…
</form>
)
Spawning Sub-Spaces
Calling subSpace
with a string will turn that attribute of a space into a child space.
e.g. Given a space called userSpace
with this state:
{
name: 'Jon',
settings: {
nightMode: true,
fontSize: 12
}
}
You can convert the settings
into a child space with userSpace.subSpace('settings')
.
Note that even though settings
is now a space, the state of userSpace
hasn't changed. At least not until the settings
space is updated with a change.
FAQ
What's the difference between a state and a space?
Think of state as an object with a bunch of values. A space contains that state, but provides a few handy methods meant for interacting with it. So if you're in a component that just needs to read from the state, then you don't need to give it a space, you can just give it the state. If that component needs to also update the state or spawn sub-spaces, then pass it the space.
How do I add middleware like in Redux?
Hopefully that feature will come in v2!
Are spaces immutable?
Sort of. The state you get from a space is an immutable object. You cannot change it directly, if you do so you may get an error. But… you can mutate the state by using the bindTo
, setState
, and replaceState
functions provided by the state's space.
Why do list items need an id
key?
Due to the fact that the state is immutable, if a sub-space for an item wants to update its state, SpaceAce needs to find it in the parent space's state. The way we've solved this is to use a unique id field. It's a similar concept to React's built-in key
prop. In fact, every space in an array that has an id
is automatically given an identical key
field for convenience.
How can I remove all the keys from an object in the state?
If you setState an empty object onto a key, it empties it out.
e.g.
space.setState({ child: { bool: true, num: 123 } });
space.setState({ child: { num: 321 } });
console.log(space.state.child); // { bool: true, num: 321 }
// To empty out "child":
space.setState({ child: {} });
console.log(space.state.child); // {}
License
MIT
Author
Created with the best of intentions by Jon Abrams