Skip to content

Commit f1fd96f

Browse files
authoredAug 22, 2019
Merge pull request #20 from XbyOrange/v1.1.0
V1.1.0
2 parents 08a3f63 + a22123d commit f1fd96f

8 files changed

+286
-67
lines changed
 

Diff for: ‎.travis.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ script:
2020
- npm run lint
2121
- npm run test-ci
2222
- npm run build
23-
# - npm run coveralls
23+
- npm run coveralls
2424
- 'if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then sonar-scanner; fi'
2525

2626
deploy:

Diff for: ‎CHANGELOG.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1111
### Removed
1212
### BREAKING CHANGES
1313

14+
## [1.1.0] - 2019-08-21
15+
### Added
16+
- Add server side data behavior tests and documentation.
17+
1418
## [1.1.0-beta.2] - 2019-08-19
1519
### Added
16-
- Add helper for registering sources that should be loaded during server side data rendering.
20+
- Add method for registering sources that should be loaded during server side data rendering.
1721

1822
### Fixed
1923
- Fix server side data connect. Load data in client when no server data is available

Diff for: ‎README.md

+65
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,71 @@ export const ConnectedModule = connect(
177177

178178
> This parser function should not be used as a replacement to Mercury Selectors. As a good practice, the preference is to use Selectors if possible instead of this function.
179179
180+
## Prefetching data on server side rendering
181+
182+
Methods for prefetching data on server side rendering are available too. When data is prefetched in server side, the connect function will pass the `value` property calculated on server side to the components directly. It will not modify the `loading` property until the first load on client side is finished (At first client-side load, the resource will not be considered as `loading` to maintain the server-side value in the component until it finish loading).
183+
184+
### Server side data methods and components
185+
186+
* `addServerSideData(source)`
187+
* Arguments
188+
* source - `<Object> or <Array> of <Objects>` Mercury source or sources that should be read when `readServerSideData` method is executed. Can be Mercury origins or selectors of any type, queried or not.
189+
* `readServerSideData([source])`
190+
* Arguments
191+
* source - `<Object> or <Array> of <Objects>` Mercury source or sources that should be read when `readServerSideData` method is executed. Can be Mercury origins or selectors of any type, queried or not.
192+
* Returns
193+
* `<Object>` This method is asynchronous, and returns an object containing all server side data ready to be set on the `<ServerSideData>` context component.
194+
* `<ServerSideData data={data} clientSide={true}><App/></ServerSideData>` Component that sets the result of the `readServerSideData` method in a context to make it available from all mercury connected children components.
195+
* Props
196+
* data - `<Object>` Object containing the result of the `readServerSideData` method.
197+
* clientSide - `<Boolean>` If false, the connect method will not dispatch automatically the read method of the sources marked as "server-side", so, for example, api requests will not be repeated on client side, and data retrieved in server side will be always passed to connected components.
198+
199+
### Example of server side prefecth implementation in a Next project:
200+
201+
In the next example, the data of the "myDataSource" mercury source will be fetched on server side and request will not be repeated on client side. The component will be rendered directly with server side data, and no loading state will be set:
202+
203+
```jsx
204+
// src/home.js
205+
import { addServerSideData, connect } from "@xbyorange/react-mercury";
206+
import { myDataSource } from "src/data";
207+
208+
addServerSideData(myDataSource); // source is marked to be read when readServerSideData method is executed.
209+
210+
export const HomeComponent = ({data}) => {
211+
if(data.loading) {
212+
return <div>Loading...</div>
213+
}
214+
return <div>{data.value}</div>
215+
};
216+
217+
export const mapDataSourceToProps = () => ({
218+
data: myDataSource.read
219+
});
220+
221+
export const Home = connect(mapDataSourceToProps)(HomeComponent)
222+
223+
```
224+
225+
```jsx
226+
// pages/index.js
227+
import { readServerSideData, ServerSideData } from "@xbyorange/react-mercury";
228+
import { Home } from "src/home";
229+
230+
const Page = ({ serverSideData }) => (
231+
<ServerSideData data={serverSideData} clientSide={false} >
232+
<Home/>
233+
</ServerSideData>
234+
);
235+
236+
Page.getInitialProps = async () => {
237+
return {
238+
serverSideData: await readServerSideData()
239+
}
240+
}
241+
242+
export default Page;
243+
```
244+
180245
## Demo
181246

182247
To run a real implementation example in a React app, you can clone the project and execute the provided demo:

Diff for: ‎jest.config.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ module.exports = {
1717
// An object that configures minimum threshold enforcement for coverage results
1818
coverageThreshold: {
1919
global: {
20-
branches: 75,
21-
functions: 75,
22-
lines: 80,
23-
statements: 80
20+
branches: 100,
21+
functions: 100,
22+
lines: 100,
23+
statements: 100
2424
}
2525
},
2626

Diff for: ‎package-lock.json

+8-50
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: ‎package.json

+4-2
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@
3232
"coveralls": "cat ./coverage/lcov.info | coveralls"
3333
},
3434
"peerDependencies": {
35-
"react": "^16.7.0"
35+
"react": "^16.7.0",
36+
"@xbyorange/mercury": "^1.1.0"
3637
},
3738
"dependencies": {
3839
"hoist-non-react-statics": "^3.2.1",
@@ -42,7 +43,8 @@
4243
"@babel/core": "^7.4.5",
4344
"@babel/preset-env": "^7.4.5",
4445
"@babel/preset-react": "^7.0.0",
45-
"@xbyorange/mercury-api": "1.0.1",
46+
"@xbyorange/mercury-api": "1.2.0",
47+
"@xbyorange/mercury": "^1.1.0",
4648
"axios": "^0.19.0",
4749
"axios-retry": "^3.1.2",
4850
"babel-core": "^7.0.0-bridge.0",

Diff for: ‎src/readServerSideData.js

+8-8
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,15 @@ const resultsToObject = results => {
2222
};
2323

2424
export const addServerSideData = sources => {
25-
const sourcesToAdd = isArray(sources) ? sources : [sources];
26-
sourcesToAdd.forEach(source => {
27-
serverSideData.add(source);
28-
});
25+
if (sources) {
26+
const sourcesToAdd = isArray(sources) ? sources : [sources];
27+
sourcesToAdd.forEach(source => {
28+
serverSideData.add(source);
29+
});
30+
}
2931
};
3032

31-
export const readServerSideData = (...args) => {
32-
args.forEach(source => {
33-
addServerSideData(source);
34-
});
33+
export const readServerSideData = sources => {
34+
addServerSideData(sources);
3535
return Promise.all(Array.from(serverSideData).map(getSourceData)).then(resultsToObject);
3636
};

Diff for: ‎test/connect.spec.js

+191-1
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ import Enzyme, { shallow, mount } from "enzyme";
33
import Adapter from "enzyme-adapter-react-16";
44
import PropTypes from "prop-types";
55
import AxiosMock from "./Axios.mock.js";
6+
import { Selector } from "@xbyorange/mercury";
67
import { Api } from "@xbyorange/mercury-api";
7-
import { connect } from "../src/index";
8+
import sinon from "sinon";
9+
import { connect, readServerSideData, addServerSideData, ServerSideData } from "../src/index";
810

911
import jsdom from "jsdom";
1012
const { JSDOM } = jsdom;
@@ -81,19 +83,35 @@ describe("react connect plugin", () => {
8183
author: "Hemingway"
8284
}
8385
];
86+
let sandbox;
8487
let books;
88+
let booksServerSide;
8589
let axios;
8690

8791
beforeAll(() => {
92+
sandbox = sinon.createSandbox();
8893
axios = new AxiosMock();
8994
books = new Api("/books", {
9095
defaultValue: []
9196
});
97+
booksServerSide = new Api("/books", {
98+
defaultValue: []
99+
});
100+
92101
books.client = axios._stub;
102+
booksServerSide.client = axios._stub;
103+
sandbox.spy(booksServerSide, "read");
104+
105+
addServerSideData(booksServerSide);
93106
});
94107

95108
afterAll(() => {
96109
axios.restore();
110+
sandbox.restore();
111+
});
112+
113+
afterEach(() => {
114+
sandbox.reset();
97115
});
98116

99117
beforeEach(() => {
@@ -134,6 +152,178 @@ describe("react connect plugin", () => {
134152
wrapper.unmount();
135153
});
136154

155+
it("if source is not server side, it should pass source properties to component and update value when finish loading even when clientSide is disabled in context", async () => {
156+
expect.assertions(2);
157+
const mapDataSourceToProps = () => ({
158+
books: books.read
159+
});
160+
const ConnectedBooks = connect(mapDataSourceToProps)(BooksList);
161+
162+
const wrapper = mount(
163+
<ServerSideData data={{}} clientSide={false}>
164+
<ConnectedBooks />
165+
</ServerSideData>
166+
);
167+
books.clean();
168+
wrapper.update();
169+
expect(wrapper.find(".loading").length).toEqual(1);
170+
await books.read();
171+
wrapper.update();
172+
expect(wrapper.find("li.book").length).toEqual(2);
173+
wrapper.unmount();
174+
});
175+
176+
it("should render serverSide properties, update value when finish loading, and do not update loading property", async () => {
177+
expect.assertions(6);
178+
const mapDataSourceToProps = () => ({
179+
books: booksServerSide.read
180+
});
181+
const ConnectedBooks = connect(mapDataSourceToProps)(BooksList);
182+
183+
const serverSideData = await readServerSideData();
184+
185+
const wrapper = mount(
186+
<ServerSideData data={serverSideData} clientSide={true}>
187+
<ConnectedBooks />
188+
</ServerSideData>
189+
);
190+
191+
expect(wrapper.find(".loading").length).toEqual(0);
192+
expect(wrapper.find("li.book").length).toEqual(2);
193+
194+
booksServerSide.read();
195+
wrapper.update();
196+
expect(wrapper.find(".loading").length).toEqual(0);
197+
expect(wrapper.find("li.book").length).toEqual(2);
198+
199+
await booksServerSide.read();
200+
wrapper.update();
201+
expect(wrapper.find(".loading").length).toEqual(0);
202+
expect(wrapper.find("li.book").length).toEqual(2);
203+
wrapper.unmount();
204+
});
205+
206+
it("should render serverSide properties, update value when finish loading, and do not update loading property when passing a selector", async () => {
207+
expect.assertions(6);
208+
const booksServerSideSelector = new Selector(booksServerSide, result => result, []);
209+
addServerSideData(booksServerSideSelector);
210+
const mapDataSourceToProps = () => ({
211+
books: booksServerSideSelector.read
212+
});
213+
const ConnectedBooks = connect(mapDataSourceToProps)(BooksList);
214+
215+
const serverSideData = await readServerSideData();
216+
217+
const wrapper = mount(
218+
<ServerSideData data={serverSideData}>
219+
<ConnectedBooks />
220+
</ServerSideData>
221+
);
222+
223+
expect(wrapper.find(".loading").length).toEqual(0);
224+
expect(wrapper.find("li.book").length).toEqual(2);
225+
226+
booksServerSideSelector.read();
227+
wrapper.update();
228+
expect(wrapper.find(".loading").length).toEqual(0);
229+
expect(wrapper.find("li.book").length).toEqual(2);
230+
231+
await booksServerSideSelector.read();
232+
wrapper.update();
233+
expect(wrapper.find(".loading").length).toEqual(0);
234+
expect(wrapper.find("li.book").length).toEqual(2);
235+
wrapper.unmount();
236+
});
237+
238+
it("serverSideData behavior should work when adding many sources to readServerSideData", async () => {
239+
expect.assertions(6);
240+
const booksServerSideSelector = new Selector(booksServerSide, result => result, []);
241+
addServerSideData([booksServerSideSelector, booksServerSide]);
242+
const mapDataSourceToProps = () => ({
243+
books: booksServerSideSelector.read
244+
});
245+
const ConnectedBooks = connect(mapDataSourceToProps)(BooksList);
246+
247+
const serverSideData = await readServerSideData(booksServerSideSelector);
248+
249+
const wrapper = mount(
250+
<ServerSideData data={serverSideData} clientSide={true}>
251+
<ConnectedBooks />
252+
</ServerSideData>
253+
);
254+
255+
expect(wrapper.find(".loading").length).toEqual(0);
256+
expect(wrapper.find("li.book").length).toEqual(2);
257+
258+
booksServerSideSelector.read();
259+
wrapper.update();
260+
expect(wrapper.find(".loading").length).toEqual(0);
261+
expect(wrapper.find("li.book").length).toEqual(2);
262+
263+
await booksServerSideSelector.read();
264+
wrapper.update();
265+
expect(wrapper.find(".loading").length).toEqual(0);
266+
expect(wrapper.find("li.book").length).toEqual(2);
267+
wrapper.unmount();
268+
});
269+
270+
it("serverSideData behavior should work with queried selectors and getters", async () => {
271+
expect.assertions(6);
272+
const booksServerSideSelector = new Selector(booksServerSide, result => result, []);
273+
const queriedBooksServerSideSelector = booksServerSideSelector.query("foo");
274+
addServerSideData(queriedBooksServerSideSelector);
275+
const mapDataSourceToProps = () => ({
276+
booksValue: queriedBooksServerSideSelector.read.getters.value,
277+
booksLoading: queriedBooksServerSideSelector.read.getters.loading,
278+
booksError: queriedBooksServerSideSelector.read.getters.error
279+
});
280+
const ConnectedBooks = connect(mapDataSourceToProps)(BooksList);
281+
282+
const serverSideData = await readServerSideData();
283+
284+
const wrapper = mount(
285+
<ServerSideData data={serverSideData} clientSide={true}>
286+
<ConnectedBooks />
287+
</ServerSideData>
288+
);
289+
290+
expect(wrapper.find(".loading").length).toEqual(0);
291+
expect(wrapper.find("li.book").length).toEqual(2);
292+
293+
queriedBooksServerSideSelector.read();
294+
wrapper.update();
295+
expect(wrapper.find(".loading").length).toEqual(0);
296+
expect(wrapper.find("li.book").length).toEqual(2);
297+
298+
await queriedBooksServerSideSelector.read();
299+
wrapper.update();
300+
expect(wrapper.find(".loading").length).toEqual(0);
301+
expect(wrapper.find("li.book").length).toEqual(2);
302+
wrapper.unmount();
303+
});
304+
305+
it("should render serverSide properties and do not dispath read if clientSide is disabled", async () => {
306+
expect.assertions(3);
307+
const mapDataSourceToProps = () => ({
308+
books: booksServerSide.read
309+
});
310+
const ConnectedBooks = connect(mapDataSourceToProps)(BooksList);
311+
312+
const serverSideData = await readServerSideData();
313+
314+
const wrapper = mount(
315+
<ServerSideData data={serverSideData} clientSide={false}>
316+
<ConnectedBooks />
317+
</ServerSideData>
318+
);
319+
320+
expect(wrapper.find(".loading").length).toEqual(0);
321+
expect(wrapper.find("li.book").length).toEqual(2);
322+
expect(booksServerSide.read.callCount).toEqual(1);
323+
324+
wrapper.unmount();
325+
});
326+
137327
it("should be able to pass string properties to component", async () => {
138328
expect.assertions(2);
139329
const mapDataSourceToProps = () => ({

0 commit comments

Comments
 (0)
Please sign in to comment.