user1941537
user1941537

Reputation: 6695

How to convert React app to React-Redux app?

I created a very simple app with only React and now I want to change it so, I can use Redux in it. (I know Redux is not needed here, but I'm just trying to learn Redux).

SearchBar.js - Only React

import React, { Component } from "react";

class SearchBar extends Component {
  state = {
    inputValue: ""
  };

  handleFormSubmit = e => {
    e.preventDefault();
    this.props.onSubmit(this.state.inputValue);
  };

  render() {
    return (
      <div className="header">
        <h1>Search for images on Unsplash</h1>
        <form onSubmit={this.handleFormSubmit} className="ui form">
          <input
            type="text"
            placeholder="Type here to search for images"
            value={this.state.inputValue}
            onChange={e => this.setState({ inputValue: e.target.value })}
          />
        </form>
      </div>
    );
  }
}

export default SearchBar;

App.js - Only React

import React, { Component } from "react";
import axios from "axios";
import SearchBar from "./components/SearchBar";
import ImageList from "./components/ImageList";

class App extends Component {
  state = {
    images: []
  };

  onSearchSubmit = async inputValue => {
    const API_KEY =
      "<MY API KEY FOR UNSPLASH>";

    const response = await axios.get(
      `https://api.unsplash.com/search/photos?page=1&query=${inputValue}&client_id=${API_KEY}`
    );

    this.setState({ images: response.data.results });
  };

  render() {
    return (
      <>
        <SearchBar onSubmit={this.onSearchSubmit} />
        <div>
          <ImageList images={this.state.images} />
        </div>
      </>
    );
  }
}

export default App;

Using Redux

I put the redux-version on codeSandBox. Of course it's not working yet.

Here are my changes so far:

App.js with redux

import React, { Component } from "react";
import { Provider } from "react-redux";
import store from "./store";

import SearchBar from "./components/SearchBar";
import ImageList from "./components/ImageList";
import "./app.scss";

class App extends Component {
  render() {
    return (
      <Provider store={store}>
        <SearchBar onSubmit={this.onSearchSubmit} />
        <div>
          <ImageList images={this.state.images} />
        </div>
      </Provider>
    );
  }
}

export default App;

store.js

import { createStore, applyMiddleware, compose } from "redux";
import thunk from "redux-thunk";
import rootReducer from "./reducers";

const initialState = {};

const middleware = [thunk];

const store = createStore(
  rootReducer,
  initialState,
  compose(
    applyMiddleware(...middleware),
    window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
  )
);

export default store;

fetchAction.js

import axios from "axios";

export const FETCH_DATA = "fetch_data";

// Getting all images
export const getImages = inputValue => async dispatch => {
  const API_KEY =
    "<MY API KEY FOR UNSPLASH>";
  const res = await axios.get(
    `https://api.unsplash.com/search/photos?page=1&query=${inputValue}&client_id=${API_KEY}`
  );
  console.log(res.data.results);

  dispatch({
    type: FETCH_DATA,
    payload: res.data.results
  });
};

index.js inside reducers folder

import { combineReducers } from "redux";
import fetchReducer from "./fetchReducer";

export default combineReducers({
  images: fetchReducer
});

fetchReducer.js

import { FETCH_DATA } from "../actions/fetchAction";

const initialState = {};

export default function(state = initialState, action) {
  switch (action.type) {
    case FETCH_DATA:
      return {
        ...state
      };

    default:
      return state;
  }
}

But, I have two questions:

  1. Where should I use connect? in App.js or in SearchBar.js?
  2. If I add the following to my component, where I use connect:

    const mapStateToProps = state => ({ images: });

    export default connect( mapStateToProps, { getImages } )(SearchBar);

What would be the value of images inside mapStateToProps?

I put the redux-version on codeSandBox. Of course it's not working yet.

Upvotes: 1

Views: 2207

Answers (3)

Daniel
Daniel

Reputation: 15453

So for your store.js, instead of:

import { createStore, applyMiddleware, compose } from "redux";
import thunk from "redux-thunk";
import rootReducer from "./reducers";

const initialState = {};

const middleware = [thunk];

const store = createStore(
  rootReducer,
  initialState,
  compose(
    applyMiddleware(...middleware),
    window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
  )
);

export default store;

try:

import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import { createStore, applyMiddleware } from "redux";
import thunk from 'redux-thunk';

import App from "./components/App";
import reducers from "./reducers";

const store = createStore(reducers, applyMiddleware(thunk));

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.querySelector("#root")
);

Then in your App.js file, instead of:

import React, { Component } from "react";
import { Provider } from "react-redux";
import store from "./store";
import SearchBar from "./components/SearchBar";
import ImageList from "./components/ImageList";
import "./app.scss";

class App extends Component {
  render() {
    return (
      <Provider store={store}>
        <SearchBar onSubmit={this.onSearchSubmit} />
        <div>
          <ImageList images={this.state.images} />
        </div>
      </Provider>
    );
  }
}

export default App;

try:

import React, { Component } from "react";
import SearchBar from "./components/SearchBar";
import ImageList from "./components/ImageList";
import "./app.scss";

class App extends Component {
  render() {
    return (
      <div>
        <SearchBar onSubmit={this.onSearchSubmit} />
        <div>
          <ImageList images={this.state.images} />
        </div>
      </div>
    );
  }
}

export default App;

For your Axios request, instead of throwing all that code inside of fetchActions.js, create a folder/file structure apis/unsplash.js:

import axios from 'axios';

export default axios.create({
    baseURL: 'https://api.unsplash.com'
});

Then inside your fetchActions.js:

export const FETCH_DATA = "fetch_data";

// Getting all images
export const getImages = inputValue => async dispatch => {
  const API_KEY =
    "<MY API KEY FOR UNSPLASH>";
  const res = await unsplash.get(
    `/search/photos?page=1&query=${inputValue}&client_id=${API_KEY}`
  );
  console.log(res.data.results);

  dispatch({
    type: "FETCH_DATA",
    payload: res.data.results
  });
};

Your combineReducers is looking good. Your fetchReducer.js, I see this often in commercial applications: const initialState = {};

It's not really necessary, cut that initialState middleman out and just:

import { FETCH_DATA } from "../actions/fetchAction";

export default function(state = {}, action) {
  switch (action.type) {
    case "FETCH_DATA":
      return {
        ...state
      };

    default:
      return state;
  }
}

Nice, clean, elegant. Now, where to use connect? in App or SearchBar component? Let's ask ourselves, what is the purpose of the connect function?

Let's see, we created our Redux store and then passed it off to the Provider Why did we do that? Ahh, yes, so that any component inside our application can gain access to the Redux store through the Provider tag.

In other words access some data. So what component needs to access some data? App? Nah, not really, its the parent component in the hierarchy and just keeps all the other components wired up nicely, right?

But our SearchBar, we are going to be accessing some data via that search bar, right?

Now, the SearchBar component may not be the only component where you might need the connect function but I would start there by importing:

import { connect } from 'react-redux';

at the top of SearchBar, then at the bottom of SearchBar I would implement:

export default connect()(SearchBar)

What in the world?! Why do we have a second set of parentheses around SearchBar?!

As I had tried to explain to a former student who complained to the administrators that I was incompetent for offering that very line of code you see above, that line of code is no different than doing this:

function connect() {
  return function() {
    return 'howdy!';
  }
}
connect();

Oh my, a function that returns a function, but wait, that doesn't print out anything!

But if I immediately add on a second parentheses:

function connect() {
      return function() {
        return 'howdy!';
      }
    }
    connect()();

I get the print out of howdy!

All we are doing here is returning a function and when we call the function that gets returned we place the second set of parentheses after it, so the second set of parentheses invokes the function that got returned.

In regards to your mapStateToProps, there is something missing there, I believe it should look like:

const mapStateToProps = state => {
  return { images: state.images };
};

Upvotes: 0

TRomesh
TRomesh

Reputation: 4481

1) you should use connect in SearchBar 2) This depends on the place where you call the API call to fetch images. Try something like this you will have to call this.props.getImages(..) inside your Gallery component, most probably when you type in something

import React, { Component } from "react";
import { connect } from "react-redux";
import { getImages } from "../actions/fetchAction";
import Masonry from "react-masonry-component";

const masonryOptions = {
  transitionDuration: 1
};

const imagesLoadedOptions = { background: ".my-bg-image-el" };

class Gallery extends Component {
  childElements = () => {
    if (this.props.images) {
      return this.props.images.map(item => {
        return (
          <li className="masonry_item" key={item.id}>
            <img
              src={item.urls.regular}
              alt={item.description}
              className="masonry_item_img"
            />
          </li>
        );
      });
    }
    return <div>no images</div>;
  };

  render() {
    console.log(this.props);
    // map method generates a new Array

    return (
      <Masonry
        className={"masonry"} // default ''
        elementType={"ul"} // default 'div'
        options={masonryOptions} // default {}
        disableImagesLoaded={false} // default false
        updateOnEachImageLoad={false} // default false and works only if disableImagesLoaded is false
        imagesLoadedOptions={imagesLoadedOptions} // default {}
      >
        {this.childElements()}
      </Masonry>
    );
  }
}

let mapStateToProps = (state, props) => {
  return {
    images: state.images.images
  };
};

let mapDispatchToProps = dispatch => {
  return {
    getImages: data => {
      dispatch(getImages(data));
    }
  };
};

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Gallery);

Upvotes: 0

Rhyso
Rhyso

Reputation: 696

The advised standard/ a common process is to set your connected logic inside a separate container file. This will contain you connect function as well as any mapStateToProps, mapDispatchToProps etc

Your container could then look like this:

import SearchBar from "./components/SearchBar"

const mapStateToProps = state => ({ 
  images: images(state)
});

export default connect(mapStateToProps)(SearchBar)

Upvotes: 0

Related Questions