UNPKG

14.8 kBMarkdownView Raw
1Composi
2=======
3
4Contents
5--------
6- [Installation](../README.md)
7- [JSX](./jsx.md)
8- [Hyperx](./hyperx.md)
9- [Hyperscript](./hyperscript.md)
10- [Functional Components](./functional-components.md)
11- [Mount and Render](./render.md)
12- [Components](./components.md)
13- State
14- [Lifecycle Methods](./lifecycle.md)
15- [Events](./events.md)
16- [Styles](./styles.md)
17- [Unmount](./unmount.md)
18- [Third Party Libraries](./third-party.md)
19- [Deployment](./deployment.md)
20- [Differrences with React](./composi-react.md)
21
22State
23-----
24
25Components can be stateless or stateful. There are advantages to both. Chose the right one depending on use case. For example, if a component will only display the result of some other action, make it stateless and pass it the data it needs. If the data of a component can change at unpredictable times, make it stateful. That way, when its data changes, it will update automatically. If you have a fixed dataset and the component will never need to be rendered again during the session, make it stateless and pass it the data.
26
27When you give a component state, Composi assigns it getters and setters. Setting state causes the component to render a new virtual DOM, diff it with the old virtual DOM, and if there are differences, update the actual DOM. As such, the assignment of data to a component's state will trigger an update. Setting state directly is ideal for primitive data types, such as string, numbers:
28
29```javascript
30const helloWorld = new Component({
31 root: '#hello',
32 state: 'World',
33 render: (message) => (
34 <h1>Hello, {message}!</h1>
35 )
36})
37// Inject component in DOM:
38helloWorld.update()
39```
40
41With the above component, we can change its output by directly accessing the component's state:
42
43```javascript
44helloWorld.state = 'everybody'
45```
46
47Booleans
48---------
49By default boolean values are not output. This is so they can be used for conditional checks. However, if you want to output a boolean value, just convert it to a string. There are two ways to do this. For `true` or `false` you can add `toString()` to convert them to strings. The values `null` and `undefined` do not have a `toString()` function, but you can use string concatenation to convert them, or use `String(value)`. Below are examples of these approaches:
50
51```javascript
52// For true or false:
53render: (value) => <p>The boolean value is: {value.toString()}</p>
54
55// The above approach would throw an error if the boolean was undefined or null.
56// For them, do the following:
57render: (value) => <p>The boolean value is: {value + ''}</p>
58
59// Make boolean uppercase:
60render: (value) => <p>The boolean value is: {(String(value).toUpperCase()}</p>
61```
62
63Complex Data Types
64------------------
65Most components will have state of complex data types: objects or arrays. To update the state of complex types you have two choices: use the `setState` method or get the state, perform your operations and set the state with the final result. We'll look at `setState` first.
66
67setState
68--------
69The Component class has a method called `setState`. Actually, you can use `setState` to set the state of primitive types. So, with the helloWorld component above, we could update its state like this:
70
71```javascript
72// Do not use this:
73helloWorld.state = 'everybody'
74
75// Instead do this:
76helloWorld.setState('everybody')
77```
78
79But `setState` is really about making it easy to update the state of complex types. Lets look at a component with an object for its state:
80
81```javascript
82const personComponent = new Component({
83 root: '#personDiv',
84 state: {
85 firstName: 'Joe',
86 lastName: 'Bodoni',
87 job: 'Mechanic',
88 age: 23
89 },
90 render: (person) => (
91 <div>
92 <p>Name: {}</p>
93 <p>Job{}</p>
94 </div>
95 )
96})
97```
98
99Because this component's state is complex, we cannot directly update the state properties:
100
101```javascript
102// This assignment will not work:
103personComponent.state.job = 'Web Developer'
104```
105
106Instead we need to use the `setState` method. To update a state object property, we pass in an object with that property and the new value. Behind the scenes, `setState` will mixin the new object with the state object. Then it will create a new virtual DOM and patch the actual DOM to reflect the changes.
107
108```javascript
109// Update the job property of the component's state:
110personComponent.setState({job: 'Web Developer'})
111```
112
113Updating Array State
114--------------------
115
116When a component has an array as state, you need to provide a second argument for `setState`: the index of the array where you want to make the index. For instance, suppose we have a component `fruitList` that prints out a list of fruits. We notice that the third item in the list is mispelled and want to update it. We can do that as follows:
117
118```javascript
119const fruitList = new Component({
120 container: 'main',
121 state: ['Apples', 'Oranges', 'Pinapplez', 'Bananas'],
122 render: (fruits) => {
123 return (
124 <ul>
125 {
126 fruits.map(fruit => <li>{fruit}</li>)
127 }
128 </ul>
129 )
130 }
131})
132
133// Use second argument for index in the array you want to update:
134fruitList.setState(prevState => {
135 prevState[2] = 'Pinapples'
136 // Return new state to update component:
137 return prevState
138})</code>
139```
140
141### Arrays of Objects
142Arrays of objects are more complicated. This is because when you use `setState` on an array, it is actually performing a splice operation on the array. That means that if you only pass in an object with the property you want to update, as we did for objects, the entire object at that position in the array will be replaced by what you provided. Therefore to update an object in an array you need to provide a complete object for the update, or update the object first and then pass it in. Let's suppose we have an array of people objects:
143
144```javascript
145const people = [
146 {
147 firstName: 'Joe',
148 lastName: 'Bodoni',
149 job: 'Mechanic'
150 },
151 {
152 firstName: 'Ellen',
153 lastName: 'Vanderbilt',
154 job: 'Lab Technician'
155 },
156 {
157 firstName: 'Sam',
158 lastName: 'Anderson',
159 job: 'Web Developer'
160 }
161]
162```
163This array is used as the state for a component of users. We want to update Joe's job to Rock Star. You might try to do this:
164
165```javascript
166// Update job for Joe Bodoni:
167userList.setState({job: 'Rock Star'}, 0)
168```
169
170The above operation will not update the user Joe Bodoni as you might think. It will result in the user's first and last names being deleted. Instead, we need to use a function to get the state and update the user's job at the correct index of the state object:
171
172```javascript
173// Proper way to update state. Use setState to access the index of array, make change and return it:
174userList.setState(prevState => {
175 // Update job of first person object in array:
176 prevState[0].job = 'Rock Star'
177 // Return changed state to update component:
178 return prevState
179})
180```
181
182Complex State Operations
183------------------------
184
185As we saw in our last example of arrays, sometimes you will need to get the state, operate on it separately and then set the component's state to that. For example, if you need to use map, filter, sort or reverse on an array, you'll want to get the complete state and perform these operations. Aftwards you can just set the state:
186
187```javascript
188// Use setState with a callback to get the state, reverse it and return it:
189fruitsList.setState(prevState => {
190 prevState.reverse()
191 return prevState
192})
193```
194
195setState with a Callback
196------------------------
197When you pass a callback to the `setState` method, the first argument of the callback will be the component's state. In the example below, notice how we get the state and manipulate it in the `handleClick` method. After doing what you need to with state, remember to return it. Otherwise the component's state will not get updated.
198
199```javascript
200class Button extends Component {
201 constructor(props) {
202 super(props)
203 this.state = { counter: 1 }
204 }
205 render() {
206 return <button onclick={() => this.handleClick()}>{this.state.counter}</button>
207 }
208 handleClick() {
209 // Use callback in setState to manilate state before retuning it:
210 this.setState(state => {
211 if (state.counter < 10) {
212 return {counter: state.counter + 1}
213 }
214 })
215 }
216}
217
218// Create instance of button:
219// Clicking on the new button will encrease its value upto 10.
220const button = new Button()
221```
222
223Be aware that whatever the callback returns will be set as the component's new state. Therefore you must complete all the changes you need before returning it.
224
225Keyed Items
226-----------
227
228In general, Composi's diffing algorythm is very efficient at determining what changed. However, when dealing with long lists, especially if the items order has changed in a random way, it can be challenging to determine the best way to patch the DOM. Keys provide a mechnamism to remedy this. If you are not going to make any drastic changes to a list's data, keys are not necessary. But if the order of updated items can change dramatically, you'll want to use keyed data.
229
230### Keyed Array Data
231The easiest way to create a keyed list is to have any array of data with unique ids. These ids only need to be unique for the items in the array the list is using. You can use any scheme to create unique ids for array items. You could use a `uuid` module from [NPM](https://www.npmjs.com/search?q=uuid).
232
233Below is an example of some keyed data:
234
235```javascript
236const fruits = [
237 {
238 id: 'a101',
239 name: 'Apple',
240 price: '1.00'
241 },
242 {
243 id: 'a102',
244 name: 'Orange',
245 price: '1.50'
246 },
247 {
248 id: 'a103',
249 name: 'Banana',
250 price: '2.00'
251 }
252]
253```
254
255To create a keyed list we need to provide each item in the list a `key` property. Notice how the following template does that:
256
257```javascript
258const fruitList = new Component({
259 root: '#fruits',
260 state: fruits,
261 render: (fruits) => (
262 <ul>
263 {
264 fruits.map(fruit => <li key={fruit.id}>{fruit.name}</li>)
265 }
266 </ul>
267 )
268})
269```
270
271The diffing algorythm will use the key value to understand the order of items in the list. This results in more efficient diffing and patching. Putting the `key` properting in the markup of a list will not be rendered to the DOM. The `key` property will only exist in the virtual DOM, where it is used to determine if the order of list elements has changed.
272
273Keys Must Be Unique
274-------------------
275When using keys, make sure that each key is unique to that dataset. Otherwise, if they are not the diff and patch algorythms will get confused and produced unpredictable results when patching the DOM. You can use whatever means you want to create keys as long as they are unique.
276
277State in Extended Components
278----------------------------
279
280When you extend Component to create a specialized class, you may want to set initial state for all instances of the class. You can set state directly in the constructor using the `this` keyword:
281
282```javascript
283class Clock extends Component {
284 constructor(opts) {
285 super(opts)
286 // Set state for all class instances:
287 this.state = {time: Date.now()
288 }
289 render() {
290 let time = this.state.time
291 const angle = (time) => 2 * Math.PI * time / 60000
292 return (
293 <li>
294 <div>
295 <svg viewBox="0 0 100 100" width="150px">
296 <circle cx="50" cy="50" r="45" fill="blue" />
297 <line
298 stroke-width="2"
299 x1="50"
300 y1="50"
301 x2={ 50 + 40 * Math.cos(angle(time)) }
302 y2={ 50 + 40 * Math.sin(angle(time)) }
303 stroke="white"
304 />
305 </svg>
306 </div>
307 </li>
308 )
309 }
310}
311```
312
313
314Third Party State Management
315----------------------------
316Because you can create stateless components, you can use thrid party state management solutions with Composi. Redux, Mobx, or roll your own.
317
318Redux
319-----
320
321```javascript
322const { h, Component } from 'composi'
323const { createStore } from 'redux'
324
325// Reducer:
326function count(state=0, action) {
327 switch(action.type) {
328 case 'INCREMENT':
329 if (state > 99) return 100
330 return state + 1
331 case 'DECREMENT':
332 if (state < 1) return 0
333 return state - 1;
334 default:
335 return state
336 }
337}
338
339// Action Creators:
340const increment = () => {
341 return {
342 type: 'INCREMENT'
343 };
344};
345
346const decrement = () => {
347 return {
348 type: 'DECREMENT'
349 };
350};
351
352// Create Redux store:
353const store = createStore(count)
354
355
356// Extend Component to create counter:
357class Counter extends Component {
358 constructor(opts) {
359 super(opts)
360
361 // Assigning store to component:
362 this.store = opts.store
363
364 // Update component when store state changes:
365 store.subscribe(() => this.updateFromStore())
366
367 // Give counter default value of "0":
368 this.render = (count = 0) => (
369 <div id="counter">
370 <button id="dec" disabled={count==0} onclick={() => this.dec()}>-</button>
371 <span id="text">{count}</span>
372 <button id="inc" onclick={() => this.inc()}>+</button>
373 </div>
374 )
375 }
376
377 // Use ths method to udpate counter with state changes:
378 updateFromStore() {
379 const state = this.store.getState()
380 this.update(state)
381 }
382
383 inc() {
384 this.store.dispatch(increment())
385 }
386
387 dec() {
388 this.store.dispatch(decrement())
389 }
390}
391
392// Create new counter:
393const counter = new Counter({
394 root: 'article',
395 store: store
396})
397counter.update()
398```
399
400This was a trivial example of using Redux with Composi. Please consult [Redux documention](http://redux.js.org/docs/basics/) to learn more about how Redux can solve your state management needs.
401
402Mobx
403----
404
405You can also use [Mobx](https://mobx.js.org) for state management. Like in the Redux example above, you'll want to use state components.
406
407```javascript
408const {h, Component} from 'composi'
409const { observable, autorun } from 'mobx'
410
411const store = observable({ count: 0})
412
413// Extend Component to create counter:
414class Counter extends Component {
415 constructor(opts) {
416 super(opts)
417 this.root = 'article'
418
419 // Assign Mobx obersable store to Counter:
420 this.store = store
421
422 // Give counter default value of "0":
423 this.render = (count = 0) => (
424 <div id="counter">
425 <button id="dec" onclick={() => this.dec()} disabled={count==0}>-</button>
426 <span id="text">{count}</span>
427 <button id="inc" onclick={() => this.inc()} disabled={count>19}>+</button>
428 </div>
429 )
430 }
431
432 inc() {
433 this.store.count++
434 }
435
436 dec() {
437 this.store.count--
438 }
439}
440
441// Create new counter:
442const counter = new Counter()
443
444// Capture observable store changes and update counter:
445autorun(() => counter.update(counter.store.count))
446```
447
448Of course, Mobx has many more useful and powerful features. This was just a trivial example. Consult the [Mobx documentation](https://mobx.js.org/refguide/api.html) to learn more about how Mobx can solve your state management needs.