Skip to content

Commit 1a8ae56

Browse files
committed
Huge rewrite/refactor of the application.
Adds support for Preact (including "native" Preact - i.e. without preact-compat) Adds support for React's "Portals" feature. Breaking Change: componentWillMount now gets executed before the visitor. This should allow for better intialized instances being passed into the visitor. Hopefully this doesn't affect you, but it is good to be made aware of this change. Fixes the manner in which context is passed down the tree.
1 parent 45493e8 commit 1a8ae56

File tree

6 files changed

+659
-473
lines changed

6 files changed

+659
-473
lines changed

README.md

Lines changed: 170 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# react-tree-walker 🌲
22

3-
Walk a React element tree, executing a provided visitor function against each element.
3+
Walk a React (or Preact) element tree, executing a "visitor" function against each element.
44

55
[![npm](https://img.shields.io/npm/v/react-tree-walker.svg?style=flat-square)](http://npm.im/react-tree-walker)
66
[![MIT License](https://img.shields.io/npm/l/react-tree-walker.svg?style=flat-square)](http://opensource.org/licenses/MIT)
@@ -9,92 +9,208 @@ Walk a React element tree, executing a provided visitor function against each el
99

1010
## TOCs
1111

12-
- [Introduction](#introduction)
13-
- [Example](#example)
14-
- [FAQs](#faqs)
12+
* [Introduction](#introduction)
13+
* [Illustrative Example](#illustrative-example)
14+
* [Order of Execution](#order-of-execution)
15+
* [API](#api)
1516

1617
## Introduction
1718

18-
Originally inspired/lifted from the awesome [`react-apollo`](https://github.com/apollostack/react-apollo) project.
19+
Inspired/lifted from the awesome [`react-apollo`](https://github.com/apollostack/react-apollo) project. 😗
1920

20-
This modified version expands upon the design, making it `Promise` based, allowing the visitor to return a `Promise`, which would subsequently delay the tree walking until the `Promise` is resolved. The tree is still walked in a depth-first fashion.
21+
This modified version expands upon the design, making it `Promise` based, allowing the visitor to return a `Promise`, which would subsequently delay the tree walking until the `Promise` is resolved. The tree is still walked in a depth-first fashion.
2122

22-
With this you could, for example, perform pre-rendering parses on your React element tree to do things like data prefetching. 🤛
23+
With this you could, for example, perform pre-rendering parses on your React element tree to do things like data prefetching. Which can be especially helpful when dealing with declarative APIs such as the one provided by React Router 4.
2324

24-
# Example
25+
# Illustrative Example
2526

26-
In the below example we walk the tree and execute the `getValue` function on every element instance that has the function available. We then push the value into a values array.
27+
In the below example we will create a visitor that will walk a React application, looking for any "class" component that has a `getData` method on it. We will then execute the `getData` function, storing the results into an array.
2728

2829
```jsx
29-
import reactTreeWalker from 'react-tree-walker';
30+
import reactTreeWalker from 'react-tree-walker'
3031

31-
class Foo extends React.Component {
32+
class DataFetcher extends React.Component {
3233
constructor(props) {
33-
super(props);
34-
this.getData = this.getData.bind(this);
34+
super(props)
35+
this.getData = this.getData.bind(this)
3536
}
3637

3738
getData() {
38-
// Return a promise or a sync value
39-
return Promise.resolve(this.props.value);
39+
// Supports promises! You could call an API for example to fetch some
40+
// data, or do whatever "bootstrapping" you desire.
41+
return Promise.resove(this.props.id)
4042
}
4143

4244
render() {
43-
return <div>{this.props.children}</div>;
45+
return <div>{this.props.children}</div>
4446
}
4547
}
4648

4749
const app = (
4850
<div>
4951
<h1>Hello World!</h1>
50-
<Foo value={1} />
51-
<Foo value={2}>
52-
<Foo value={4}>
53-
<Foo value={5} />
54-
</Foo>
55-
</Foo>
56-
<Foo value={3} />
52+
<DataFetcher id={1} />
53+
<DataFetcher id={2}>
54+
<DataFetcher id={3}>
55+
<DataFetcher id={4} />
56+
</DataFetcher>
57+
</DataFetcher>
58+
<DataFetcher id={5} />
5759
</div>
58-
);
59-
60-
const values = [];
61-
62-
/**
63-
* Visitor to be executed on each element being walked.
64-
*
65-
* @param element - The current element being walked.
66-
* @param instance - If the current element is a Component or PureComponent
67-
* then this will hold the reference to the created
68-
* instance. For any other element type this will be null.
69-
* @param context - The current "React Context". Any provided childContextTypes
70-
* will be passed down the tree.
71-
*
72-
* @return Anything other than `false` to continue walking down the current branch
73-
* OR
74-
* `false` if you wish to stop the traversal down the current branch,
75-
* OR
76-
* `Promise<true|false>` a promise that resolves to either true/false
77-
*/
78-
function visitor(element, instance, context) {
60+
)
61+
62+
const values = []
63+
64+
// You provide this! See the API docs below for full details.
65+
function visitor(element, instance) {
7966
if (instance && typeof instance.getData) {
80-
return instance.getData()
81-
.then((value) => {
82-
values.push(value);
83-
// Return "false" to indicate that we do not want to traverse "4"'s children
84-
return value !== 4
85-
})
67+
return instance.getData().then(value => {
68+
values.push(value)
69+
// Return "false" to indicate that we do not want to visit "3"'s children,
70+
// therefore we do not expect "4" to make it into our values array.
71+
return value !== 3
72+
})
8673
}
8774
}
8875

8976
reactTreeWalker(app, visitor)
9077
.then(() => {
91-
console.log(values); // [1, 2, 4, 3];
78+
console.log(values) // [1, 2, 3, 5];
79+
// Now is a good time to call React's renderToString whilst exposing
80+
// whatever values you built up to your app.
9281
})
9382
// since v3.0.0 you need to do your own error handling!
94-
.catch(err => console.error(err));
83+
.catch(err => console.error(err))
84+
```
85+
86+
Not a particularly useful piece of code, but hopefully it is illustrative enough as to indicate the posibilities. One could use this to warm a cache or a `redux` state, subsequently performing a `renderToString` execution with all the required data in place.
9587

88+
## Order of Execution
89+
90+
`react-tree-walker` walks your React application in a depth-first fashion, i.e. from the top down, visiting each child until their are no more children available before moving on to the next element. We can illustrate this behaviour using the below example:
91+
92+
```jsx
93+
<div>
94+
<h1>Foo</h1>
95+
<section>
96+
<p>One</p>
97+
<p>Two</p>
98+
</section>
99+
<Footer />
100+
</div>
96101
```
97102

98-
## FAQs
103+
In this example the order of elements being visited would be:
104+
105+
div -> h1 -> "Foo" -> section -> p -> "One" -> p -> "Two" -> Footer
106+
107+
Whilst your application is being walked its behaviour will be much the same as if it were being rendered on the server - i.e. the `componentWillMount` lifecycle will be executed for any "class" components, and context provided by any components will be passed down and become available to child components.
108+
109+
Despite emulating a server side render, the tree walking process is far cheaper as it doesn't actually perform any rendering of the element tree to a string. It simply interogates your app building up an object/element tree. The really expensive cycles will likely be the API calls that you make. 😀
110+
111+
That being said you do have a bail-out ability allowing you to suspend the traversal down a branch of the tree. To do so you simply need to return `false` from your visitor function, or if returning a `Promise` ensure that the `Promise` resolves a `false` for the same behaviour.
112+
113+
## API
114+
115+
The API is very simple at the moment, only exposing a single function, which you can import as follows
116+
117+
### `react-tree-walker` (`Function`)
118+
119+
```
120+
const reactTreeWalker = require('react-tree-walker')
121+
```
122+
123+
_or_
124+
125+
```
126+
import reactTreeWalker from 'react-tree-walker'
127+
```
128+
129+
**Paramaters**
130+
131+
* **tree** (React/Preact element, _required_)
132+
133+
The react application you wish to walk.
134+
135+
e.g. `<div>Hello world</div>`
136+
137+
* **visitor** (`Function`, _required_)
138+
139+
The function you wish to execute against _each_ element that is walked on the `tree`.
140+
141+
See its [API docs](#visitor) below.
142+
143+
* **context** (`Object`, _optional_)
144+
145+
Any root context you wish to provide to your application.
146+
147+
e.g. `{ myContextItem: 'foo' }`
99148

100-
> Let me know if you have any...
149+
* **options** (`Object`, _optional_)
150+
151+
Additional options/configuration. It currently supports the following values:
152+
153+
* _componentWillUnmount_: Enable this to have the `componentWillUnmount` lifecycle event be executed whilst walking your tree. Defaults to `false`. This was added as an experimental additional flag to help with applications where they have critical disposal logic being executed within the `componentWillUnmount` lifecycle event.
154+
155+
**Returns**
156+
157+
A `Promise` that resolves when the tree walking is completed.
158+
159+
### `visitor`
160+
161+
Encapsulates the logic you wish to execute against each element. You create and provide this function to the `reactTreeWalker`.
162+
163+
**Parameters**
164+
165+
* **element** (React/Preact element, _required_)
166+
167+
The current element being walked.
168+
169+
* **instance** (Component Instance, _optional_)
170+
171+
If the current element being walked is a "class" Component then this will contain the instance of the Component - allowing you to interface with its methods etc.
172+
173+
* **context** (`Object`, _required_)
174+
175+
The React context that is available to the current element. `react-tree-walker` emulates React in exposing context down the tree.
176+
177+
* **childContext** (`Object`, _optional_)
178+
179+
If the current element being walked is a "class" Component and it exposes additional "child" context (via the `getChildContext` method) then this will contain the context that is being provided by the component instance.
180+
181+
**Returns**
182+
183+
If you return `false` then the children of the current element will not be visited.
184+
185+
e.g.
186+
187+
```javascript
188+
function visitor(element) {
189+
if (element.type === 'menu') {
190+
// We will not traverse the children for any <menu /> nodes
191+
return 'false'
192+
}
193+
}
194+
```
195+
196+
You can also return a `Promise` which will cause the tree walking to wait for the `Promise` to be resolved before attempting to visit the children for the current element.
197+
198+
```javascript
199+
function visitor(element, instance) {
200+
// This will make every visit take 1 second to execution.
201+
return new Promise(resolve => setTimeout(resolve, 1000))
202+
}
203+
```
204+
205+
You can make the Promise resolve a `false` to indicate that you do not want the children of the current element to be visited.
206+
207+
```javascript
208+
function visitor(element, instance) {
209+
// Only the first element will be executed, and it will take 1 second to complete.
210+
return (
211+
new Promise(resolve => setTimeout(resolve, 1000))
212+
// This prevents any walking down the current elements children
213+
.then(() => false)
214+
)
215+
}
216+
```

package.json

Lines changed: 13 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,22 @@
11
{
22
"name": "react-tree-walker",
33
"version": "3.0.0",
4-
"description": "Walk a React element tree, executing a provided function against each node.",
4+
"description":
5+
"Walk a React element tree, executing a provided function against each node.",
56
"license": "MIT",
67
"main": "commonjs/index.js",
7-
"files": [
8-
"*.js",
9-
"*.md",
10-
"umd",
11-
"commonjs"
12-
],
8+
"files": ["*.js", "*.md", "umd", "commonjs"],
139
"repository": {
1410
"type": "git",
1511
"url": "https://github.com/ctrlplusb/react-tree-walker.git"
1612
},
1713
"homepage": "https://github.com/ctrlplusb/react-tree-walker#readme",
1814
"author": "Sean Matheson <[email protected]>",
19-
"keywords": [
20-
"react",
21-
"react-element",
22-
"util",
23-
"tree",
24-
"visitor"
25-
],
15+
"keywords": ["react", "react-element", "util", "tree", "visitor"],
2616
"scripts": {
2717
"build": "node ./tools/scripts/build.js",
28-
"clean": "rimraf ./commonjs && rimraf ./umd && rimraf ./coverage && rimraf ./umd",
18+
"clean":
19+
"rimraf ./commonjs && rimraf ./umd && rimraf ./coverage && rimraf ./umd",
2920
"lint": "eslint src",
3021
"precommit": "lint-staged && npm run test",
3122
"prepublish": "npm run build",
@@ -65,6 +56,7 @@
6556
"in-publish": "2.0.0",
6657
"jest": "^21.2.1",
6758
"lint-staged": "^4.2.3",
59+
"preact": "^8.2.7",
6860
"prettier": "^1.7.4",
6961
"pretty-bytes": "4.0.2",
7062
"prop-types": "^15.6.0",
@@ -79,12 +71,8 @@
7971
"rollup-plugin-uglify": "^3.0.0"
8072
},
8173
"jest": {
82-
"collectCoverageFrom": [
83-
"src/**/*.{js,jsx}"
84-
],
85-
"snapshotSerializers": [
86-
"<rootDir>/node_modules/enzyme-to-json/serializer"
87-
],
74+
"collectCoverageFrom": ["src/**/*.{js,jsx}"],
75+
"snapshotSerializers": ["<rootDir>/node_modules/enzyme-to-json/serializer"],
8876
"testPathIgnorePatterns": [
8977
"<rootDir>/(commonjs|coverage|flow-typed|node_modules|tools|umd)/"
9078
]
@@ -98,21 +86,16 @@
9886
"node": true,
9987
"jest": true
10088
},
101-
"extends": [
102-
"airbnb",
103-
"prettier"
104-
],
89+
"extends": ["airbnb", "prettier"],
10590
"rules": {
10691
"camelcase": 0,
10792
"import/prefer-default-export": 0,
10893
"import/no-extraneous-dependencies": 0,
94+
"no-nested-ternary": 0,
10995
"no-underscore-dangle": 0,
11096
"react/no-array-index-key": 0,
11197
"react/react-in-jsx-scope": 0,
112-
"semi": [
113-
2,
114-
"never"
115-
],
98+
"semi": [2, "never"],
11699
"react/forbid-prop-types": 0,
117100
"react/jsx-filename-extension": 0,
118101
"react/sort-comp": 0
@@ -132,9 +115,6 @@
132115
"trailingComma": "all"
133116
},
134117
"lint-staged": {
135-
"*.js": [
136-
"prettier --write \"src/**/*.js\"",
137-
"git add"
138-
]
118+
"*.js": ["prettier --write \"src/**/*.js\"", "git add"]
139119
}
140120
}

0 commit comments

Comments
 (0)