Skip to content

Commit 608e646

Browse files
authored
Merge pull request #5 from itReverie/playingWithState
Creating an external store to manage the state.
2 parents 6e797f4 + a511edf commit 608e646

File tree

7 files changed

+215
-12
lines changed

7 files changed

+215
-12
lines changed

lib/components/App.js

Lines changed: 70 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,93 @@
22
// I have my normal class component CLIENT SIDE
33
import React from 'react';
44
import PropTypes from 'prop-types';
5+
import pickBy from 'lodash.pickby';
56

67
import ArticleList from './ArticleList';
8+
import SearchBar from './SearchBar';
9+
import Timestamp from './Timestamp';
710

811
class App extends React.Component {
912

1013
//****** I need the following lines to expose the CONTEXT
1114
//to make the context api work we need to define the context type
12-
static childContextTypes={
15+
static childContextTypes = {
1316
store: PropTypes.object
1417
};
18+
1519
//Getting the context
16-
getChildContext(){
20+
getChildContext() {
1721
return {
18-
store:this.props.store};
22+
store: this.props.store
23+
};
1924
}
25+
2026
//********
2127

22-
state= this.props.store.getState();
28+
state = this.props.store.getState();
29+
30+
onStoreChange=()=>{
31+
this.setState(this.props.store.getState);
32+
}
33+
34+
componentDidMount(){
35+
//we need to update the component state so the app component state is in sync to the store state
36+
if(this.subsciptionId) {
37+
this.subsciptionId = this.props.store.subscribe(this.onStoreChange);
38+
}
39+
this.props.store.startClock();
40+
}
41+
42+
componentWillUnmount(){
43+
this.props.store.unsubscribe(this.subscriptionId);
44+
this.subscriptionId = null;
45+
}
46+
47+
//In react we cannot return two elements unless
48+
//Option 1. is in an array so we need to pass a Key elements
49+
// render() {
50+
// return [
51+
// <SearchBar key="searchBar"/>,
52+
// <ArticleList key="articleList"
53+
// articles={this.state.articles}
54+
// store={this.props.store}
55+
// />
56+
// ];
57+
// }
58+
59+
//Option 2. Wrap those components in a div
60+
// return (
61+
// <div>
62+
// <SearchBar key="searchBar" doSearch={this.props.store.setSearchTerm}/>
63+
// <ArticleList key="articleList" articles={articles}
64+
// store={this.props.store}/>
65+
// </div>
66+
// );
67+
68+
69+
// We can just update when the store state changes..this means we need to subscribe
70+
// with a callback
71+
// we can make a StateApi an event emmiter (Flux)
72+
// or manage subscriptions in the same object
2373

2474
render() {
75+
let { articles , searchTerm}=this.state;
76+
//making the search case insensitive
77+
const searchRegex = new RegExp(searchTerm, 'i');
78+
if(searchTerm){
79+
articles= pickBy(articles, (value) => {
80+
return value.title.match(searchRegex) ||
81+
value.body.match(searchRegex);
82+
});
83+
}
2584
return (
26-
<ArticleList
27-
articles={this.state.articles}
28-
store={this.props.store}
29-
/>
85+
<div>
86+
<Timestamp />
87+
<SearchBar key="searchBar" />
88+
<ArticleList key="articleList"
89+
articles={articles}
90+
/>
91+
</div>
3092
);
3193
}
3294
}

lib/components/SearchBar.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import React from 'react';
2+
import debounce from 'lodash.debounce';//NOTE: we are just importing the debounce function from lodash not the whole package
3+
import storeProvider from './storeProvider';
4+
5+
class SearchBar extends React.Component{
6+
7+
state={
8+
searchTerm: ''
9+
};
10+
11+
//A delay so I dont see every character is type but the end result
12+
doSearch = debounce(()=> {
13+
this.props.store.setSearchTerm(this.state.searchTerm);
14+
},300)
15+
16+
17+
handleSearch = (event) =>{
18+
this.setState({ searchTerm: event.target.value}, ()=> {
19+
this.doSearch();
20+
});
21+
22+
};
23+
24+
25+
//Option 1. We could read the value of the text typed by ref (getting the current DOM node)
26+
// ref={(input) => this.searchInput = input}
27+
//However, Option 2. It is more advisable to use the state for that
28+
render(){
29+
return (<input
30+
type="search"
31+
placeholder="Enter search term"
32+
value={this.state.searchTerm}
33+
onChange={this.handleSearch}/>);
34+
}
35+
}
36+
37+
//Connecting to the store
38+
export default (storeProvider())(SearchBar);

lib/components/Timestamp.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
2+
import React from 'react';
3+
import storeProvider from './storeProvider';
4+
5+
class Timestamp extends React.Component{
6+
render(){
7+
return(<div>{this.props.timestamp.toString()}</div>);
8+
}
9+
}
10+
11+
function extraProps(store)
12+
{
13+
return {
14+
timestamp: store.getState().timestamp
15+
};
16+
}
17+
18+
export default storeProvider(extraProps) (Timestamp);

lib/components/storeProvider.js

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,32 @@ import PropTypes from 'prop-types';
77
//The new component could have a couple of new things as well
88
//Having a function that calls another function in the arrow functions
99
//Allow us to pass more parameters extra props
10-
const storeProvider = (extraProps) => (Component) => {
10+
//extraProps = () =>({}) We are making extra props a default parameter of empty if they dont pass me extra props.
1111

12-
return class extends React.Component{
12+
const storeProvider = (extraProps = () =>({})) => (Component) => {
13+
14+
return class extends React.PureComponent{
1315
static displayName = `${Component.name}Container`;
1416
static contextTypes = {store: PropTypes.object};
1517

18+
19+
onStoreChange=()=>{
20+
//Give the component the signal that it needs to re-render
21+
this.forceUpdate();
22+
}
23+
24+
componentDidMount(){
25+
//we need to update the component state so the app component state is in sync to the store state
26+
if(this.subsciptionId) {
27+
this.subsciptionId = this.context.store.subscribe(this.onStoreChange);
28+
}
29+
}
30+
31+
componentWillUnmount(){
32+
this.context.store.unsubscribe(this.subscriptionId);
33+
this.subscriptionId = null;
34+
}
35+
1636
render(){
1737
return <Component
1838
{...this.props}

lib/state-api/lib/index.js

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
1-
//we are just converting the array data into object data
21
class StateApi{
2+
33
constructor( rawData){
4+
//we are converting the array data into object data
45
this.data = {
56
articles: this.mapIntoObject(rawData.articles),
6-
authors: this.mapIntoObject(rawData.authors)};
7+
authors: this.mapIntoObject(rawData.authors),
8+
searchTerm :'',
9+
timestamp: new Date()};
10+
11+
this.subscriptions ={};
12+
13+
this.lastSubscriptionId=0;
14+
15+
716
}
817

18+
19+
920
mapIntoObject(arr){
1021
return arr.reduce( (acc, curr)=> {
1122
acc[curr.id] = curr;
@@ -20,9 +31,53 @@ class StateApi{
2031
}
2132

2233
// getState(){
34+
//Return the data in objects
2335
getState= ()=>{
2436
return this.data;
2537
}
38+
39+
//The call back is what the subscriber is interested to be executed.
40+
//Every time that data changes, please call this call back
41+
subscribe =(callback) => {
42+
this.lastSubscriptionId ++;
43+
this.subscriptions[this.lastSubscriptionId] = callback;
44+
return this.lastSubscriptionId;
45+
};
46+
47+
//I am leaving the DOM tree so do not bother in notifying me of changes
48+
//Remove me from your list
49+
unsubscribe =(subscriptionId) => {
50+
delete this.subscriptions[subscriptionId];
51+
};
52+
53+
//Notify to all the places where we are using the state (data) that there have been changes
54+
//We do that because react is a one way binding so we need to let him know
55+
notifySubscribers =()=>{
56+
Object.values(this.subscriptions).forEach((callback) => callback());
57+
};
58+
59+
//Updatet the current state with the new state changes
60+
//Notify everybody
61+
mergeWithState = (stateChange) =>{
62+
this.data = {...this.data, ...stateChange};
63+
this.notifySubscribers();
64+
};
65+
66+
67+
//if the state gets bigger or there are too many actions
68+
//it is better to deal with the sate separate than the app.js
69+
setSearchTerm = (searchTerm) => {
70+
71+
this.mergeWithState({
72+
searchTerm,
73+
});
74+
};
75+
76+
startClock = ()=>{
77+
setInterval(()=>{this.mergeWithState({timestamp: new Date()
78+
});
79+
}, 1000);
80+
};
2681
}
2782

2883
export default StateApi;

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@
5656
"babel-preset-stage-2": "^6.24.1",
5757
"ejs": "^2.5.7",
5858
"express": "^4.16.2",
59+
"lodash.debounce": "^4.0.8",
60+
"lodash.pickby": "^4.6.0",
5961
"pm2": "^2.7.2",
6062
"prop-types": "^15.6.0",
6163
"react": "^16.0.0",

yarn.lock

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3297,6 +3297,10 @@ locate-path@^2.0.0:
32973297
p-locate "^2.0.0"
32983298
path-exists "^3.0.0"
32993299

3300+
lodash.debounce@^4.0.8:
3301+
version "4.0.8"
3302+
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
3303+
33003304
lodash.findindex@^4.4.0:
33013305
version "4.6.0"
33023306
resolved "https://registry.yarnpkg.com/lodash.findindex/-/lodash.findindex-4.6.0.tgz#a3245dee61fb9b6e0624b535125624bb69c11106"
@@ -3313,6 +3317,10 @@ lodash.merge@^4.6.0:
33133317
version "4.6.0"
33143318
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.0.tgz#69884ba144ac33fe699737a6086deffadd0f89c5"
33153319

3320+
lodash.pickby@^4.6.0:
3321+
version "4.6.0"
3322+
resolved "https://registry.yarnpkg.com/lodash.pickby/-/lodash.pickby-4.6.0.tgz#7dea21d8c18d7703a27c704c15d3b84a67e33aff"
3323+
33163324
lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.3.0:
33173325
version "4.17.4"
33183326
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"

0 commit comments

Comments
 (0)