Server
We start the server side.
At the GET verb of any url..we already have code where we store initial state in our redux. This time, let’s start it off by fetching data.
We create an initial state object, when store fetched data into an array called repositoryNames. Remember that in order to fetch in Node, we need to either use axios, or node-fetch package.
server.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
... import thunk from 'redux-thunk'; import axios from 'axios'; ... app.get('*', async (req, res) => { const scripts = ['vendor.js', 'client.js']; let initialState; if (req.url === '/') { const repositories = await axios.get('https://jsonplaceholder.typicode.com/todos'); console.log('http get data'); initialState = { repositoryNames: repositories.data.map((repo) => ({ id: repo.id, name: repo.title, })), }; } const store = createStore(reducers, initialState, applyMiddleware(thunk)); ... ... ... |
Notice thunk. Redux Thunk is a middleware that lets you call action creators that return a function instead of an action object. That function receives the store’s dispatch method, which is then used to dispatch regular synchronous actions inside the function’s body once the asynchronous operations have been completed.
Once that is done, we create our store, and place it in Provider component. Then place that into StaticRouter within our appMarkup.
1 2 3 4 5 6 7 8 |
const store = createStore(reducers, initialState, applyMiddleware(thunk)); const appMarkup = ReactDOMServer.renderToString( <StaticRouter location={req.url} context={{}}> <Provider store={store}> <App /> </Provider> </StaticRouter> ); |
This is so that we can inject it into our HTML component.
1 2 3 4 5 6 7 |
const html = ReactDOMServer.renderToStaticMarkup( <Html children={appMarkup} scripts={scripts} initialState={initialState} /> ); |
We then return this HTML component in our response object to the client.
1 2 3 4 5 6 7 8 9 |
const html = ReactDOMServer.renderToStaticMarkup( <Html children={appMarkup} scripts={scripts} initialState={initialState} /> ); console.log('html', new Date()); res.send(`<!doctype html>${html}`); |
Client Side
We do the same and apply thunk for the client. Since we already have the store and initial app state, it will match up with what the server has.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
... import thunk from 'redux-thunk'; import { applyMiddleware, createStore } from 'redux'; ... const store = createStore(reducers, { ...window.APP_STATE }, applyMiddleware(thunk)); ReactDOM.hydrate( <BrowserRouter> <Provider store={store}> <App /> </Provider> </BrowserRouter>, document.getElementById('app') ); |
Before in App.js, where we declare our mapping to props with initialText and changeText handler, we dispatch a very simple Action object with a literal object like so:
1 2 3 |
const mapDispatchToProps = (dispatch) => ({ changeText: () => dispatch({ type: 'CHANGE_TEXT' }), }); |
which then get passed to reducers/index.js to be processed like so. The initialText is a simple string so we can do this:
1 2 3 4 5 6 7 8 9 10 |
function reducer(state, action) { switch (action.type) { case 'CHANGE_TEXT': return { ...state, initialText: 'changed in the browser!' }; default: return { ...state }; } } export default reducer; |
But now, we use action objects defined in actions/index.js for more complicated procedures:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import axios from 'axios'; export const getRepositoryNames = () => (dispatch) => { axios .get('https://api.github.com/repositories') .then(({ data }) => { dispatch({ type: 'SET_REPOSITORY_NAMES', payload: data.map((repo) => ({ id: repo.id, name: repo.title, })), }); }); }; |
Thus, now in Home.js, we declare mapDispatchToProps with the action object getRepositoryNames:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import { getRepositoryNames } from '../actions'; ... const Home = ({ repositoryNames, getRepositoryNames }) => { ... }; const mapStateToProps = ({ repositoryNames }) => ({ repositoryNames, }); const mapDispatchToProps = (dispatch) => ({ getRepositoryNames: () => dispatch(getRepositoryNames()), }); export default connect(mapStateToProps, mapDispatchToProps)(Home); |
ref – https://reactjs.org/docs/hooks-effect.html
That way, we can use getRepositoryNames inside of Home component. Since Home is a functional component, we use useEffectuseEffect runs after every render!
By default, it runs both after the first render and after every update. Instead of thinking in terms of “mounting” and “updating”, you might find it easier to think that effects happen “after render”. React guarantees the DOM has been updated by the time it runs the effects.
This is the basic case in our example.
Hence we check for props repositoryNames. If its valid, we’ll display it. If it isn’t, we’ll fetch the data so it can be rendered.
Also, useEffect has a nifty feature where it can look at a state variable. If that state changes, it will run the effect. In our case, it would fetch. Hence, I put two use cases of this:
1) value changes after each click. useEffect will run every time the button is clicked because onClick updates value everytime.
2) value changes once after clicks. So useEffect will run the first time, value updates, runs second time, and that’s it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
const Home = ({ repositoryNames, getRepositoryNames }) => { console.log('Home', new Date()); const [value, setValue] = useState(true); const onClick = useCallback((a, b) => { /* // always run useEffect because value is always changing let changedValue = !value; setValue(changedValue); */ setValue(false); // value changed once. useEffect will that one time. // Then it will not run anymore because value will stay at 'false'. console.log('value', value); }); useEffect(() => { console.log('---------> useEffect', new Date()); if (!repositoryNames) { console.log('repositoryNames DNE exists, lets read it'); getRepositoryNames(); } else { console.log('repositoryNames already exists'); } }, [value]); return ( <div> <button onClick={onClick}>heheh</button> <ul> {repositoryNames && repositoryNames.map((repo) => ( <li key={repo.id}> {repo.name} </li> ))} </ul> </div> ); }; |