I'm trying to make a simple React+Redux app that gets data from the server. I understand basic principles of the Redux, but I cannot find anywhere in tutorials an example of how to implement something like "loading data on-demand".
My API can return list of the books, like this:
[
{
title: 'The Hobbit',
authorId: 1
},
{
title: 'The Colour of Magic',
authorId: 2
}
]
And there is another call to get data by specific author, this call will return something like this (by authorId):
{
title: 'J. R. R. Tolkien',
authorId: 1
}
or
{
title: 'Terry Pratchett',
authorId: 2
}
I have these methods in my "actions" file:
export function loadBooksSuccessfully(books) {
return {type: types.LOAD_BOOKS, books};
}
export function loadBooks() {
return function (dispatch) {
return MyApi.getBooks().then(data => {
dispatch(loadBooksSuccessfully(data));
}).catch(err => {
throw(err);
});
};
}
I call loadBooks() action in the main js file to display the list of the books when the page was loaded. My problem is that I don't know where should I request info about authors to display list in the following format:
Book: "The Hobbit"
Author: "J. R. R. Tolkien"
Book: "The Colour of Magic"
Author: "Terry Pratchett"
I have react components 'BooksList' and 'BookItem' for rendering the layout.
I pass the list of the books to the BooksList via mapStateToProps method:
class BooksList extends React.Component {
render() {
let books = this.props.books;
return <div>
{books.map((book) => {
return <BookItem key={`my-book-${book.id}`}
{...book} />
})}
</div>
}
}
function mapStateToProps(state, ownProps) {
return {
books: state.app.books
};
}
function mapDispatchToProps(dispatch) {
return {};
}
export default connect(mapStateToProps, mapDispatchToProps)(BooksList);
Here is my BookItem react component:
class BookItem extends React.Component {
public render() {
const book = this.props.book;
return (<div>
<div>
Book: {book.name}
</div>
<div>
Author: {book.author.name}
</div>
</div>);
}
}
Here is actions for loading author info:
export function loadAuthorSuccessfully(author) {
return {type: types.LOAD_AUTHOR, author};
}
export function loadAuthor(authorId) {
return function (dispatch) {
return MyApi.getAuthor(authorId).then(data => {
console.info('DATA', data);
dispatch(loadAuthorSuccessfully(data));
}).catch(err => {
throw(err);
});
};
}
I was thinking to call loadAuthor() method in the componentDidMount method of the BookItem component but it doesn't seem like real redux solution. What is the canonical way to do it?
Loading data with Redux is simply a specific case of dispatching an action.
To dispatch an action from React, you have generally two options:
1) You dispatch in some event handler - the user clicking a button or link, toggling a checkbox... a response to an event.
2) You dispatch when a specific Componente is about to mount / mounted - something appears on screen without the user needing to click around.
The first case is very simple, map your action creators using connect and call that new injected prop in your handler.
For the second, you still need to bind your action creator, and then you use one of React lifecycle events like this
class AuthorPage extends React.Component {
componentDidMount(){
this.props.loadAuthor()
}
}
This second approach means you need to take into account that the data may not be ready when the component is first rendered, so you will likely keep that in your Redux state and display a spinner or null until it is.
Hope it helps, I suggest you watch Dan Abramov's Egghead video series. 2h of your time will save you dozens at Stackoverflow. :)
Related
Typical online shopping store. I am trying to test a scenario where an AJAX request fails when the user tries to place an order.
The user can also opt-in for a subscription service but the request for this subscription should only happen if the order placement was successful.
To test this failing scenario I use a Promise rejection but the uncaught error bubbles up and causes the test to fail.
Does anyone have an idea how I can test this, preferably without adding a catch to the chaining in the onSubmit method of the Confirmation component? Does Jest or wait from react-testing-library provide alternatives methods to test this scenario?
I understand that Jest provides .rejects but I am not able to figure out how I should restructure my test to make it work with wait.
The Component:
class Confirmation extends React.Component {
onSubmit() {
const { createOrder, items, subscribeUser, userId } = this.props;
createOrder({ items })
.then(() => subscribeUser(userId));
// no catch here because I use an error boundary component
// at the the top level of the App's component tree
// to catch and log all errors to a logging service
}
render() {
return (
<div>
<input
type="submit"
onClick={this.onSubmit}
value="Confirm Order"
/>
</div>
)
}
}
The Test:
import React from 'react'
import {
render,
fireEvent,
wait
} from 'react-testing-library'
import Confirmation from './Confirmation'
describe('Confirmation', () => {
it("should not subscribe when the user's order creation fails", () => {
const props = {
userId: 12345,
items: [{
id: 121,
qty: 1
}, {
id: 122,
qty: 2
}],
createOrder: jest.fn(() => Promise.reject("Order creation failure")),
subscribeUser: jest.fn(() => {})
};
const {
container
} = render(
<Confirmation { ...props } />
);
fireEvent.click(container.querySelector("#confirm-order"));
return wait(() => {
expect(props.createOrder).toHaveBeenCalledWith({
items: props.items
});
expect(props.subscribeUser).not.toHaveBeenCalled();
});
});
});
Please note that the snippets above are not executable - in reality, the component is a bit more complex than the example above but I have tried to simplify it as much as possible without misrepresenting the problem.
Edit
Wrapping <Confirmation/> in another component with an error boundary does not seem to be working either. The error still bubbles up the component tree to fail the test:
I would create a small component that catches errors and render that one too:
const onError = jest.fn()
class Catcher extends React.Component {
componentDidCatch(error, info) {
onError(error, info)
}
render() {
return this.props.children
}
}
const { container } = render(
<Catcher>
<Confirmation { ...props } />
</Catcher>
);
// After
expect(onError).toHaveBeenCalledTimes(1)
The title is wordy, however a short / simple example will go a long ways in explaining my question. I have the following start to a component:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { fetchGames } from '../../path-to-action';
class TeamsApp extends Component {
constructor(props) {
super(props);
this.state = {
oldGames: [],
newGames: []
};
}
componentDidMount() {
this.props.dispatch(fetchGames('1617'));
this.setState({ oldGames: this.props.teamGameData });
this.props.dispatch(fetchGames('1718'));
this.setState({ newGames: this.props.teamGameData });
}
...
...
}
function mapStateToProps(reduxState) {
return {
teamGameData: reduxState.GamesReducer.sportsData
};
}
export default connect(mapStateToProps)(TeamsApp);
I would like the action / reducer that corresponds with fetchGames() and gamesReducer to be called twice when the component mounts. This action / reducer grabs some sports data, and I am trying to grab data for two separate seasons (the '1617' season and the '1718' season). The fetchGames() is built correctly to handle the season parameter.
With the current setup, the states aren't being set, and my linter is throwing an error Do not use setState in componentDidMount.
Can I pass a callback to this.props.dispatch that takes the results of the fetchGames() (the teamGameData prop), and sets the oldGames / newGames states equal to this object?
Any help with this is appreciated!
Edit: if i simply remove the this.setState()'s, then my teamGameData prop simply gets overridden with the second this.props.dispatch() call...
Edit 2: I'm not 100% sure at all if having the 2 state variables (oldGames, newGames) is the best approach. I just need to call this.props.dispatch(fetchGames('seasonid')) twice when the component loads, and have the results as two separate objects that the rest of the component can use.
Edit 3: I have the following part of my action:
export const fetchSportsDataSuccess = (sportsData, season) => ({
type: FETCH_NBA_TEAM_GAME_SUCCESS,
payload: { sportsData, season }
});
and the following case in my reducer:
case FETCH_NBA_TEAM_GAME_SUCCESS:
console.log('payload', action.payload);
return {
...state,
loading: false,
sportsData: action.payload.sportsData
};
and the console.log() looks like this now:
payload
{ sportsData: Array(2624), season: "1718" }
but i am not sure how to use the season ID to create a key in the return with this season's data....
Edit 4: found solution to edit 3 - Use a variable as an object key in reducer - thanks all for help on this, should be able to take it from here!
Copying data from the redux store to one's component state is an anti-pattern
Instead, you should modify your redux store, for example using an object to store data, so you'll be able to store datas for multiples seasons :
sportsData: {
'1617': { ... },
'1718': { ... },
}
This way you'll be able to fetch both seasons in the same time :
componentDidMount() {
const seasons = ['1718', '1617'];
const promises = seasons.map(fetchGames);
Promise.all(promises).catch(…);
}
And connect them both :
// you can use props here too
const mapStateToProps = (reduxState, props) => ({
// hardcoded like you did
oldGames: reduxState.GamesReducer.sportsData['1617'],
// or using some props value, why not
newGames: reduxState.GamesReducer.sportsData[props.newSeason],
};
Or connect the store as usual and go for the keys:
const mapStateToProps = (reduxState, props) => ({
games: reduxState.GamesReducer.sportsData,
};
…
render() {
const oldGame = this.props.games[1718];
const newGame = this.props.games[1718];
…
}
Redux is you single source of truth, always find a way to put everything you need in Redux instead of copying data in components
I am trying to create a React component that renders a table of data from a JavaScript object pixarMovies that I initially set the state of. I would like to render a table of the movie data with movies sorted chronologically by date (I've attached an image of what I'm trying to do that I created in HTML). How might one accomplish this?
This component is separate from App.js, and I will include it in a different component. I would like the table to adjust accordingly to the state (to account for the addition and removal of movies), as well as be able to apply onClick functions to the buttons.
import React, { Component } from 'react';
import { BrowserRouter as Router, Redirect, Switch, Route, Link } from 'react-router-dom';
export default class PixarMovies extends React.Component {
constructor(props) {
super(props);
this.state = {
pixarMovies: [
{
title: "Cars",
date: "2006-06-09",
budget: "$120 million",
},
{
title: "Toy Story",
date: "1995-11-05",
budget: "$30 million",
},
{
title: "Ratatouille",
date: "2007-06-29",
budget: "$150 million",
},
{
title: "The Incredibles",
date: "2004-11-05",
budget: "$92 million"
}
]
}
}
render() {
return (
<div className="container">
<h1>Pixar Movies</h1>
{/* Insert Table here */}
</div>
);
}
}
Thanks!
OK, looks like there are a couple of pieces here, so let's take a look at each of them.
Sorting chronologically by date: Javascript arrays have a sort function that looks like this: myArray.sort(). You can pass a function to the .sort call to use whatever field you want. So maybe something like this for you:
const sortedPixarMovies = this.state.pixarMovies.sort((movA, movB) => new Date(movA.date) - new Date(movB.date));
Rendering a table: There are a bunch of ways to render a table in React. I'd recommend looking at some 3rd party components that will make rendering a table quite simple and effortless. If you want to build one yourself, take a look at something like this guide. A quick google of "render a table in react" should give you all the information you need.
Table should adjust according to state: This should happen automatically. Every time your component's state changes, your component will re-render, so it will re-render the table with the new data.
Apply onClick functions to buttons: I don't see any buttons in your code, but onClick functions work in react pretty much like they would in anything else. Define a function on your component, then pass it to the button. For example:
The function:
myOnChangeFunc = (newValue) => {
this.setState({ myField: newValue });
}
And then the button:
<button onChange={ this.myOnChangeFunc }>My Button</button>
Responding to question below
If you're using a 3rd party component, usually you want to pass your data in as a prop. So your render function might look something like:
render() {
const myFormattedData = this.state.data.map... (formatted);
return <ThirdPartyComponent data = { myFormattedData } />;
}
If you're doing it yourself, you can map the data in your render function. Here's a super simple example of how you would render a list:
render() {
return (
<ul>
{ this.state.listData.map((d) => <li key={ d.id }>{ d.value }</li>) }
</ul>
);
}
So I just switched to using stateless functional components in React with Redux and I was curious about component lifecycle. Initially I had this :
// actions.js
export function fetchUser() {
return {
type: 'FETCH_USER_FULFILLED',
payload: {
name: 'username',
career: 'Programmer'
}
}
}
Then in the component I used a componentDidMount to fetch the data like so :
// component.js
...
componentDidMount() {
this.props.fetchUser()
}
...
After switching to stateless functional components I now have a container with :
// statelessComponentContainer.js
...
const mapStateToProps = state => {
return {
user: fetchUser().payload
}
}
...
As you can see, currently I am not fetching any data asynchronously. So my question is will this approach cause problems when I start fetching data asynchronously? And also is there a better approach?
I checked out this blog, where they say If your components need lifecycle methods, use ES6 classes.
Any assistance will be appreciated.
Firstly, don't do what you are trying to to do in mapStateToProps. Redux follows a unidirectional data flow pattern, where by component dispatch action, which update state, which changes component. You should not expect your action to return the data, but rather expect the store to update with new data.
Following this approach, especially once you are fetching the data asynchronously, means you will have to cater for a state where your data has not loaded yet. There are plenty of questions and tutorials out there for that (even in another answer in this question), so I won't worry to put an example in here for you.
Secondly, wanting to fetch data asynchronously when a component mounts is a common use case. Wanting to write nice functional component is a common desire. Luckily, I have a library that allows you to do both: react-redux-lifecycle.
Now you can write:
import { onComponentDidMount } from 'react-redux-lifecycle'
import { fetchUser } from './actions'
const User = ({ user }) => {
return // ...
}
cont mapStateToProps = (state) => ({
user = state.user
})
export default connect(mapStateToProps)(onComponentDidMount(fetchUser)(User))
I have made a few assumptions about your component names and store structure, but I hope it is enough to get the idea across. I'm happy to clarify anything for you.
Disclaimer: I am the author of react-redux-lifecycle library.
Don't render any view if there is no data yet. Here is how you do this.
Approach of solving your problem is to return a promise from this.props.fetchUser(). You need to dispatch your action using react-thunk (See examples and information how to setup. It is easy!).
Your fetchUser action should look like this:
export function fetchUser() {
return (dispatch, getState) => {
return new Promise(resolve => {
resolve(dispatch({
type: 'FETCH_USER_FULFILLED',
payload: {
name: 'username',
career: 'Programmer'
}
}))
});
};
}
Then in your Component add to lifecycle method componentWillMount() following code:
componentDidMount() {
this.props.fetchUser()
.then(() => {
this.setState({ isLoading: false });
})
}
Of course your class constructor should have initial state isLoading set to true.
constructor(props) {
super(props);
// ...
this.state({
isLoading: true
})
}
Finally in your render() method add a condition. If your request is not yet completed and we don't have data, print 'data is still loading...' otherwise show <UserProfile /> Component.
render() {
const { isLoading } = this.state;
return (
<div>{ !isLoading ? <UserProfile /> : 'data is still loading...' }</div>
)
}
I'm new to React and trying to develop a dashboard type application which has a number of "tiles", each displaying a single value representing a reporting metric.The tile definitions (title, etc) are read from a single JSON file at runtime. The report metric values however, require 1 remote API call per tile. This gives the user the experience of the tiles displaying immediately with the report metrics filling in as they are resolved. I'm having trouble modelling this in React. Here's a simplified version what I have so far...
// tile info object
export interface ITileData {
title: string;
value: number;
}
// tile component
export interface IDashboardTileProps {
tileData: ITileData;
}
export class DashboardTile extends React.Component<IDashboardTileProps, any> {
public render(): JSX.Element {
return (
<div>{ this.props.tileData.title } = { this.props.tileData.value }</div>
);
}
}
// container component
const TILE_DEFS: ITileData[] = this.getTileDefsFromJson();
export interface ITileContainerState {
tiles: ITileData[];
}
export class TileContainer extends React.Component<any, ITileContainerState> {
constructor() {
super();
this.state = {
tiles: TILE_DEFS
};
}
public componentDidMount() {
// load tile values
this.state.tiles.map((tile, index) => {
// *** THIS IS WHERE I NEED HELP ***
// I need to make an HTTP API call for each tile to get its value
// I don't want to call setState in a loop like this, but I do
// want the values to appear when they are resolved, not all at
// once when all of the HTTP calls are resolved.
});
}
public render(): JSX.Element {
return (
<List
items={ this.state.tiles }
onRenderCell={ tile =>
<DashboardTile tileData={ tile } />
} />
);
}
}
Any suggestions?
I have done something similar before and the way went about it was:
this.state.tiles.map should return an array of <Tile> components, you then need to make a Tile component that takes in all its needed parameters for both display and api calls. All the parent container does is provide it the props it needs to make its own api calls and manage its own state.
The benefits to this are being able to isolate those state updates to that component, have it render a loader/some sort of "I'm waiting to get results back" kind of interface per tile, and it will also display the data as soon as it gets the api back so you dont have to wait for all of them to finish.
to pseudocode:
this.state.tiles.map((tile, index) => {
return <Tile {...tile} />
}
class Tile extends Component {
constructor() {
super();
this.state = { fetching: true, data: null }
}
componentDidMount() {
// make api calls here
doApi().then((result) => {
this.setState({fetching: false, data: result})
});
}
}
I'm not familiar with the typescript or whatever you are using so this is just plain ol' react and obviously missing pieces but should give you the general sense of the idea.