Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit c3da619

Browse files
authored
Merge pull request #194 from redux-json-api/add-selectors
2 parents e02e63b + da69051 commit c3da619

File tree

6 files changed

+356
-1
lines changed

6 files changed

+356
-1
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Please raise any questions as an [Issue](https://github.com/dixieio/redux-json-a
1212
# Table of contents
1313
1. [Set-Up & Configure](docs/set-up-configure.md)
1414
1. [API](docs/api.md)
15+
1. [Selectors](docs/selectors.md)
1516
1. [Good reads](#good-reads)
1617
1. [Contribute](#contribute)
1718
1. [Contributors](#contributors)

docs/selectors.md

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
Selectors
2+
---
3+
4+
_redux-json-api_ provides a set of simple selectors.
5+
6+
#### `getResourceTree( state, resourceType: string ): JsonApiDocument`
7+
8+
Returns the state tree for the given resource type.
9+
10+
#### `getResourceData( state, resourceType: string ): array<JsonApiResource>`
11+
12+
Returns the data (i.e. array of resources) for a given resource type
13+
14+
#### `getResource( state, resourceIdentifier: JsonApiResourceIdentifier ): JsonApiResource`
15+
#### `getResource( state, resourceType: string, resourceId: string ): JsonApiResource`
16+
17+
Returns a specific resource based on the identifier, if that resource exists in the store.
18+
19+
##### Example
20+
21+
```jsx
22+
import React from 'react';
23+
import { useSelector } from 'react-redux';
24+
import { getResource } from 'redux-json-api';
25+
26+
// Where taskId is the ID for a specific task.
27+
const Task = ({ taskId }) => {
28+
const task = useSelector((state) => getResource(state, 'tasks', taskId));
29+
30+
return <li>{task.title}</li>
31+
};
32+
33+
export default Task;
34+
```
35+
36+
#### `getResources( state, resourceIdentifiers: array<JsonApiResourceIdentifier> ): array<JsonApiResource>`
37+
#### `getResources( state, resourceType: string, resourceId: array<string> ): array<JsonApiResource>`
38+
39+
Returns an array of resources based on the given identifiers.
40+
41+
##### Example
42+
43+
```jsx
44+
import React from 'react';
45+
import { useSelector } from 'react-redux';
46+
import { getResources } from 'redux-json-api';
47+
48+
// Where taskIds is an array of ids
49+
const Tasks = ({ taskIds }) => {
50+
const tasks = useSelector((state) => getResources(state, 'tasks', taskIds));
51+
52+
return <ul>
53+
{tasks.map(task => (
54+
<li>{task.title}</li>
55+
))}
56+
</ul>
57+
};
58+
59+
export default Tasks;
60+
```
61+
62+
#### `getRelatedResources( state, resource: JsonApiResourceIdentifier, relationship: string ): JsonApiResource | array<JsonApiResource>`
63+
64+
Returns the related resources for the given resource.
65+
66+
##### Example
67+
68+
```jsx
69+
import React from 'react';
70+
import { useSelector } from 'react-redux';
71+
import { getRelatedResources } from 'redux-json-api';
72+
73+
// Where user is a JsonApiResource with a "tasks" relationship
74+
const Tasks = ({ user }) => {
75+
const tasks = useSelector((state) => getRelatedResources(state, user, 'tasks'));
76+
77+
return <ul>
78+
{tasks.map(task => (
79+
<li>{task.title}</li>
80+
))}
81+
</ul>
82+
};
83+
84+
export default Tasks;
85+
```

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@
6666
"deep-equal": "^2.0.3",
6767
"object-path-immutable": "^4.1.0",
6868
"pluralize": "^8.0.0",
69-
"redux-actions": "^2.6.5"
69+
"re-reselect": "^4.0.0",
70+
"redux-actions": "^2.6.5",
71+
"reselect": "^4.0.0"
7072
}
7173
}

src/jsonapi.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createAction, handleActions } from 'redux-actions';
22
import * as imm from 'object-path-immutable';
3+
import { createCachedSelector } from 're-reselect';
34

45
import {
56
addLinksToState, ensureRelationshipInState,
@@ -605,3 +606,50 @@ export const reducer = handleActions({
605606
axiosConfig: {}
606607
}
607608
});
609+
610+
// Selectors
611+
export const getResourceTree = (state, resourceType) => (state.api[resourceType] || { data: [] });
612+
export const getResourceData = (state, resourceType) => getResourceTree(state, resourceType)?.data || [];
613+
614+
// Usage getResource(state, {type: 'users, id: '1'}) or getResource(state, 'users', '1')
615+
export const getResource = createCachedSelector(
616+
(state, identifier) => getResourceData(state, typeof identifier === 'string' ? identifier : identifier.type),
617+
(_state, identifier, id) => id || identifier.id,
618+
(resources, id) => resources.find((resource) => `${resource.id}` === `${id}`) || null
619+
)((_state, identifier, id) => (typeof identifier === 'string' ? `${identifier}/${id}` : `${identifier.type}/${identifier.id}`));
620+
621+
const getType = (identifiers) => {
622+
let type = identifiers;
623+
624+
if (Array.isArray(identifiers)) {
625+
[{ type }] = identifiers;
626+
}
627+
628+
return type;
629+
};
630+
631+
const getIdList = (identifiers, idList) => idList || identifiers.map((identifier) => identifier.id);
632+
633+
// Usage getResources(state, [{type: 'users', id: '1'}, {type: 'users', id: '2'}]) or getResources(state, 'users', ['1', '2'])
634+
export const getResources = createCachedSelector(
635+
(state, identifiers) => getResourceData(state, getType(identifiers)),
636+
(_state, identifiers, idList) => getIdList(identifiers, idList).map((id) => `${id}`),
637+
(resources, idList) => resources.filter((resource) => idList.includes(`${resource.id}`))
638+
)((_state, identifiers, idList) => {
639+
const type = getType(identifiers);
640+
const useIdList = getIdList(identifiers, idList);
641+
642+
return `${type}/${useIdList.join(':')}`;
643+
});
644+
645+
// Usage getRelatedResources(state, {type: 'users', id: '1'}, 'transactions')
646+
export const getRelatedResources = (state, identifier, relationship) => {
647+
const resource = getResource(state, identifier);
648+
649+
if (!hasOwnProperties(resource, ['relationships', relationship, 'data'])) {
650+
return null;
651+
}
652+
const relationshipData = resource.relationships[relationship].data;
653+
654+
return Array.isArray(relationshipData) ? getResources(state, relationshipData) : getResource(state, relationshipData);
655+
};

test/selectors.test.js

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import {
2+
getRelatedResources, getResource, getResourceData, getResources, getResourceTree
3+
} from '../src/jsonapi';
4+
5+
const state = {
6+
api: {
7+
endpoint: {
8+
axiosConfig: {}
9+
},
10+
users: {
11+
data: [
12+
{
13+
type: 'users',
14+
id: '1',
15+
attributes: {
16+
name: 'John Doe'
17+
},
18+
relationships: {
19+
transactions: {
20+
data: [
21+
{
22+
type: 'transactions',
23+
id: '34'
24+
}
25+
]
26+
}
27+
}
28+
},
29+
{
30+
type: 'users',
31+
id: '2',
32+
attributes: {
33+
name: 'Emily Jane'
34+
},
35+
relationships: {
36+
companies: {
37+
data: null
38+
}
39+
}
40+
}
41+
]
42+
},
43+
transactions: {
44+
data: [
45+
{
46+
type: 'transactions',
47+
id: '34',
48+
attributes: {
49+
description: 'ABC',
50+
createdAt: '2016-02-12T13:34:01+0000',
51+
updatedAt: '2016-02-19T11:52:43+0000',
52+
},
53+
relationships: {
54+
task: {
55+
data: null
56+
}
57+
},
58+
links: {
59+
self: 'http://localhost/transactions/34'
60+
}
61+
}
62+
]
63+
},
64+
isCreating: 0,
65+
isReading: 0,
66+
isUpdating: 0,
67+
isDeleting: 0
68+
}
69+
};
70+
71+
describe('Resource tree selector', () => {
72+
it('should return the full resource tree', () => {
73+
const resourceTree = getResourceTree(state, 'transactions');
74+
75+
expect(resourceTree).toEqual(state.api.transactions);
76+
});
77+
78+
it('should not break if resource does not exist', () => {
79+
const resourceTree = getResourceTree(state, 'unicorns');
80+
81+
expect(resourceTree)
82+
.toEqual({ data: [] });
83+
});
84+
});
85+
86+
describe('Resource data selector', () => {
87+
it('should return the full resource data', () => {
88+
const resourceTree = getResourceData(state, 'transactions');
89+
90+
expect(resourceTree)
91+
.toEqual(state.api.transactions.data);
92+
});
93+
94+
it('should not break if resource tree does not have a data attribute', () => {
95+
state.types = {};
96+
const resourceTree = getResourceData(state, 'types');
97+
98+
expect(resourceTree)
99+
.toEqual([]);
100+
});
101+
102+
it('should not break if resource does not exist', () => {
103+
const resourceTree = getResourceData(state, 'unicorns');
104+
105+
expect(resourceTree)
106+
.toEqual([]);
107+
});
108+
});
109+
110+
describe('Resource selector', () => {
111+
it('should return the correct resource', () => {
112+
const resourceTree = getResource(state, { type: 'users', id: '1' });
113+
114+
expect(resourceTree)
115+
.toEqual(state.api.users.data[0]);
116+
});
117+
118+
it('should support alternative inputs and the correct resource', () => {
119+
const resourceTree = getResource(state, 'users', '1');
120+
121+
expect(resourceTree)
122+
.toEqual(state.api.users.data[0]);
123+
});
124+
125+
it('should support passing an integer as an id', () => {
126+
const resourceTree = getResource(state, 'users', 1);
127+
128+
expect(resourceTree)
129+
.toEqual(state.api.users.data[0]);
130+
});
131+
132+
it('should not break if resource does not have exist', () => {
133+
state.types = {};
134+
const resourceTree = getResource(state, {
135+
type: 'users',
136+
id: '10'
137+
});
138+
139+
expect(resourceTree)
140+
.toEqual(null);
141+
});
142+
143+
it('should not break if resource type does not have exist', () => {
144+
state.types = {};
145+
const resourceTree = getResource(state, {
146+
type: 'unicorns',
147+
id: '10'
148+
});
149+
150+
expect(resourceTree)
151+
.toEqual(null);
152+
});
153+
});
154+
155+
describe('Resources selector', () => {
156+
it('should return the correct resources', () => {
157+
const resourceTree = getResources(state, 'users', ['1', '2']);
158+
159+
expect(resourceTree)
160+
.toEqual(state.api.users.data);
161+
});
162+
163+
it('should return the correct resources with an array of identifiers', () => {
164+
const resourceTree = getResources(state, [
165+
{
166+
type: 'users',
167+
id: '1',
168+
},
169+
{
170+
type: 'users',
171+
id: '2',
172+
}
173+
]);
174+
175+
expect(resourceTree)
176+
.toEqual(state.api.users.data);
177+
});
178+
179+
it('should support passing an integer as an id', () => {
180+
const resourceTree = getResources(state, 'users', [1, 2]);
181+
182+
expect(resourceTree)
183+
.toEqual(state.api.users.data);
184+
});
185+
186+
it('should not break if a resource does not have exist', () => {
187+
const resourceTree = getResources(state, 'users', ['1', '2', '3']);
188+
189+
expect(resourceTree)
190+
.toEqual(state.api.users.data);
191+
});
192+
193+
it('should not break if resource type does not have exist', () => {
194+
state.types = {};
195+
const resourceTree = getResources(state, 'unicorns', ['1']);
196+
197+
expect(resourceTree)
198+
.toEqual([]);
199+
});
200+
});
201+
202+
describe('Relationship selector', () => {
203+
it('should return the correct relationships', () => {
204+
const relationshipResources = getRelatedResources(state, state.api.users.data[0], 'transactions');
205+
206+
expect(relationshipResources)
207+
.toEqual([state.api.transactions.data[0]]);
208+
});
209+
});

yarn.lock

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4537,6 +4537,11 @@ querystring@^0.2.0:
45374537
resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
45384538
integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=
45394539

4540+
re-reselect@^4.0.0:
4541+
version "4.0.0"
4542+
resolved "https://registry.yarnpkg.com/re-reselect/-/re-reselect-4.0.0.tgz#9ddec4c72c4d952f68caa5aa4b76a9ed38b75cac"
4543+
integrity sha512-wuygyq8TXUlSdVXv2kigXxQNOgdb9m7LbIjwfTNGSpaY1riLd5e+VeQjlQMyUtrk0oiyhi1AqIVynworl3qxHA==
4544+
45404545
react-is@^16.12.0:
45414546
version "16.13.1"
45424547
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
@@ -4757,6 +4762,11 @@ require-main-filename@^2.0.0:
47574762
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
47584763
integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
47594764

4765+
reselect@^4.0.0:
4766+
version "4.0.0"
4767+
resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7"
4768+
integrity sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA==
4769+
47604770
resolve-cwd@^3.0.0:
47614771
version "3.0.0"
47624772
resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d"

0 commit comments

Comments
 (0)