I want to use the connect method from react-redux to pass the props into react hoc high order function.
The wrapper function:
export const withInfiniteScroll = (Component) =>
class WithInfiniteScroll extends React.Component {
componentDidMount() {
window.addEventListener('scroll', this.onScroll, false)
}
componentWillUnmount() {
this.props.initPage()
window.removeEventListener('scroll', this.onScroll, false)
}
onScroll = () => {
(
(window.innerHeight + window.scrollY) >= (document.body.offsetHeight - 500)
&& this.props.stocks.length
)
&& this.props.onPaginatedSearch()
}
render() {
return <Component {...this.props}/>
}
}
And the component is wrapped:
const StockCard = ({ stocks }) =>
stocks.errmsg === 'ok' ?
stocks.data.map (
(stock, i) =>
<CardContainer key={i}>
<p>{stock.cinvcode}</p>
<p>{stock.cinvname}</p>
</CardContainer>
) : ''
Get the hoc function:
const StockCardWithInfiniteScroll = withInfiniteScroll(StockCard)
The reducer:
const initState = {
...
stocks: {
errcode: '',
errmsg: '',
data: []
}
}
export const PAGE_INIT = 'PAGE_INIT'
export const initPage = () => ({type: PAGE_INIT, payload: 0})
export const STOCK_FETCH = 'STOCK_FETCH'
export const updateStock = (res) => ({type: STOCK_FETCH, payload: res})
export const onPaginated = (searchStock) => {
return(dispatch) => {
const searchStockNextPage = {
...searchStock,
pageindex: searchStock.pageindex + 1
}
// console.log(searchStockNextPage)
getStock(searchStockNextPage)
.then(res => dispatch(updateStock(res)))
.then(res => console.log(res))
}
}
const stockReducer = (state = initState, action) => {
switch (action.type) {
...
case PAGE_INIT:
return {
...state,
searchStock: {...state.searchStock, page: 0}
}
case STOCK_FETCH:
return {
...state,
stocks: state.stocks.data.concat(action.payload.data)
}
default:
return state
}
}
Last use the connect method to pass the props from redux:
export default connect(
(state) => ({
stocks: state.stock.stocks
}),
{onPaginated, initPage}
)(StockCardWithInfiniteScroll)
I can see the feedback stocks props in browser console, but the StockCard can not get it.
Related
I'm using react and redux and I'm trying a simple example.
If I clicked the button, then the number should increment by 1.
But when I currently click the button you'll see all element gonna + 1
how can i fix that?...
class Menu extends Component {
componentDidMount() {
this.props.getItems();
}
plus = () =>{
this.props.getplus();
}
render() {
const {item, count} = this.props.item
return (
<div>
{item.map(items => {
<div>{items.example}</div> <buttom onClick={this.plus}> + </button>
<div>{count}
</div> }
</div>
) }
const mapStateToProps = (state) => ({
item: state.item
})
export default connect(mapStateToProps , { getItems, getplus }) (Menu);
itemAction.js
export const getItems = () =>{
return {
type: GET_ITEMS
} }
export const getplus = () => {
return {
type: PLUS_ITEMS
} }
Reducer.js
const initialState = {
item: [
{
example:"example1"
},
{
example:"example2"
},
{
example:"example3"
},
],
count:0
}
export default function (state = initialState, action) {
switch(action.type){
case GET_ITEMS:
return {
...state
}
case PLUS_ITEMS:
return {
...state,
count:state.count + 1
}
default:
return state;
}
}
I see two errors in your code, replace this:
const {item, count} = this.props.item
with this:
const {item, count} = this.props
And map also count to your props:
const mapStateToProps = (state) => ({
item: state.item
count: state.count
})
Summary
In order to learn Redux, I am incorporating some state, actions, reducers, and trying to see how they are used in React Components.
I have set up a test object...
const initialState = {
navigationCount : 0,
someNumber : 500,
someList : ['aa',22,'c5d6','45615'],
};
...and aim to:
increment the navigationCount by 1 when visiting pages
add or subtract from someNumber
push() & pop() elements from someList.
Versions
Currently using gatsby ^2.5.0, react ^16.8.6, and react-redux ^6.0.1.
Code
actions & reducers
import { combineReducers } from 'redux';
import {
PAGE_INCREMENT,
NUMBER_INCREASE,
NUMBER_DECREASE,
LIST_PUSH,
LIST_POP,
} from './actionTypes.js';
// state
const initialState = {
navigationCount : 0,
someNumber : 500,
someList : ['aa',22,'c5d6','45615'],
};
// action creators returning actions
export const pageIncrementer = navigationCount => {
return {
type: PAGE_INCREMENT,
navigationCount,
};
};
export const numberAdder = numberToAdd => {
return {
type: NUMBER_INCREASE,
numberToAdd,
};
};
export const numberMinuser = numberToMinus => {
return {
type: NUMBER_DECREASE,
numberToMinus,
};
};
export const listPusher = itemToAdd => {
return {
type: LIST_PUSH,
itemToAdd,
}
};
export const listPopper = () => {
return {
type: LIST_POP,
}
};
// reducers
const pageIncrementReducer = (state = initialState, action) => {
switch (action.type) {
case PAGE_INCREMENT:
return Object.assign({}, ...state, {
navigationCount: action.navigationCount+1
});
default:
return state.navigationCount;
}
};
const numberChanger = (state = initialState, action) => {
switch (action.type) {
case NUMBER_INCREASE:
return Object.assign({}, ...state, {
someNumber: state.someNumber+action.numberToAdd,
});
case NUMBER_DECREASE:
return Object.assign({}, ...state, {
someNumber: state.someNumber-action.numberToMinus,
});
default:
return state.someNumber;
};
};
const listChanger = (state = initialState, action) => {
switch (action.type) {
case LIST_POP:
return Object.assign({}, ...state, {
someList: state.someList.pop(),
});
case LIST_PUSH:
return Object.assign({}, ...state, {
someList: state.someList.push(action.itemToAdd),
});
default:
return state.someList;
}
}
// store
const rootReducer = combineReducers({
pageIncrementReducer,
numberChanger,
listChanger,
});
export default rootReducer;
React Component
import React from 'react';
import Layout from '../components/common/Layout.jsx';
import LandingBanner from '../components/landing/LandingBanner.jsx';
import LandingNavgrid from '../components/landing/LandingNavgrid.jsx';
import LandingApp from '../components/landing/LandingApp.jsx';
import { connect } from 'react-redux';
import {
PAGE_INCREMENT,
NUMBER_INCREASE,
NUMBER_DECREASE,
LIST_PUSH,
LIST_POP,
} from '../state/actionTypes';
class LandingPage extends React.Component {
constructor(props){
super(props);
this.state = {
appliedNum: 2000,
};
}
componentDidMount(){
// this.props.pageIncrement(); // => numberChanger returned undefined
// this.props.numberIncrease(4444); // => pageIncrementReducer returned undefined
// this.props.numberDecrease(4444); // => pageIncrementReducer returned undefined
// this.props.listPush(4444); // => pageIncrementReducer returned undefined
this.props.listPop();
}
render(){
return (
<Layout>
<LandingBanner/>
<LandingNavgrid/>
<LandingApp/>
</Layout>
)
}
}
const filterNumbers = (list=[]) => {
console.log('filterNumbers list: ', list);
return list.filter(listElement => !!Number(listElement));
};
const mapStateToProps = (state, ownProps) => {
return {
someNumber: state.someNumber,
someList: filterNumbers(state.someList),
navigationCount: state.navigationCount,
};
};
const mapDispatchToProps = (dispatch) => {
return {
pageIncrement: () => dispatch({ type: PAGE_INCREMENT }),
numberIncrease: () => dispatch({ type: NUMBER_INCREASE }),
numberDecrease: () => dispatch({ type: NUMBER_DECREASE }),
listPush: () => dispatch({ type: LIST_PUSH }),
listPop: () => dispatch({ type: LIST_POP }),
}
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(LandingPage);
Errors
redux.js:449 Uncaught Error: Given action "LIST_POP", reducer
"pageIncrementReducer" returned undefined. To ignore an action, you
must explicitly return the previous state. If you want this reducer to
hold no value, you can return null instead of undefined.
first of all, you always need to return state on the default switch case.
default:
return state;
This is my component:
import React, { Component } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { Divider } from "antd";
import MovieList from "../components/MovieList";
import IncreaseCountButton from "../components/IncreaseCountButton";
import DeleteButton from "../components/DeleteButton";
import { deleteMovie, increaseCount } from "../actions/movies";
import { getIsDeleting, getIsIncreasing } from "../reducers/actions";
export class MovieListContainer extends Component {
constructor(props) {
super(props);
this.handleIncrease = this.handleIncrease.bind(this);
this.handleDelete = this.handleDelete.bind(this);
}
static propTypes = {
isIncreasing: PropTypes.func.isRequired,
isDeleting: PropTypes.func.isRequired,
};
async handleIncrease(movie) {
await this.props.increaseCount(movie, this.props.token);
}
async handleDelete(movie) {
await this.props.deleteMovie(movie.id, this.props.token);
}
render() {
return (
<MovieList movies={this.props.movies}>
{(text, movie) => (
<div>
<IncreaseCountButton
onIncrease={() => this.handleIncrease(movie)}
loading={this.props.isIncreasing(movie.id)}
/>
<Divider type="vertical" />
<DeleteButton
onDelete={() => this.handleDelete(movie)}
loading={this.props.isDeleting(movie.id)}
/>
</div>
)}
</MovieList>
);
}
}
export const mapStateToProps = state => ({
isIncreasing: id => getIsIncreasing(state, id),
isDeleting: id => getIsDeleting(state, id),
});
export default connect(
mapStateToProps,
{ deleteMovie, increaseCount }
)(MovieListContainer);
I feel like this might be bad for performance/reconciliation reasons, but not sure how else to retrieve the state in a way that hides implementation details.
Gist link: https://gist.github.com/vitalicwow/140c06a52dd9e2e062b2917f5c741727
Any help is appreciated.
Here is how you can handle these asynchronous actions with redux. You can use thunk to perform 2 actions and can store a flag to determine what is being done to an object (Deleting, Changing, etc):
action
export const deleteMovieAction = id => {
return dispatch => {
dispatch({ type: "MOVIE_DELETING", id });
setTimeout(() => {
dispatch({ type: "MOVIE_DELETED", id });
}, 2000);
};
};
reducer
const rootReducer = (state = initialState, action) => {
switch (action.type) {
case "MOVIE_DELETING": {
const movies = [...state.movies];
movies.find(x => x.id === action.id).isDeleting = true;
return { ...state, movies };
}
case "MOVIE_DELETED": {
const movies = state.movies.filter(x => x.id !== action.id);
return { ...state, movies };
}
default:
return state;
}
};
https://codesandbox.io/s/k3jnv01ymv
An alternative is to separate out the ids into a new array that are being deleted
const rootReducer = (state = initialState, action) => {
switch (action.type) {
case "MOVIE_DELETING": {
const movieDeletingIds = [...state.movieDeletingIds, action.id];
return { ...state, movieDeletingIds };
}
case "MOVIE_DELETED": {
const movieDeletingIds = state.movieDeletingIds.filter(
x => x.id !== action.id
);
const movies = state.movies.filter(x => x.id !== action.id);
return { ...state, movieDeletingIds, movies };
}
default:
return state;
}
};
https://codesandbox.io/s/mj52w4y3zj
(This code should be cleaned up, but is just to demo using thunk)
After changing one of my components, (Profile.js) from a class to a function to simplify and have cleaner code, the onClick triggering of a redux action (like) no longer does anything.
Some have pointed out the action needs to be map differently, but I'm not sure why as I'm still new to redux and it's confusing as to why it works fine as a class but not as a function.
What adds to the confusion is that I'm also using react thunk to make things async.
User.js
import { fetchUser, like } from '../../actions/userActions';
class User extends React.Component {
componentDidMount() {
const { username } = this.props.match.params;
this.props.fetchUser(username);
}
render() {
const { like, user } = this.props;
return (
<Profile user={user} like={like} />
)
}
}
const mapStateToProps = state => ({
user: state.store.user
});
export default connect(mapStateToProps, {fetchUser, like})(User);
Profile.js Before
import { like, user } from '../../actions/userActions';
class Profile extends React.Component {
const { like, user } = this.props
return (
<a onClick={() => like(user.username)}>Like</a>
)
}
export default connect (mapStateToProps, {like}){Profile)
Profile.js After
const Profile = (props) => {
const { like, user } = props
return (
<a onClick={() => like(user.username)}>Like</a>
)
}
actions.js
const url = 'http://localhost:3001'
function handleErrors(response) {
if (!response.ok) {
throw Error(response.statusText);
}
return response;
}
export const like = (username) => dispatch => {
fetch(`${url}/like/${username}`, {credentials: 'include', method: 'post'})
.then(handleErrors)
.then(res => res.json())
.then(res =>
dispatch({
type: LIKE,
payload: res
})
)
.catch(error => console.error('Error:', error))
}
export const fetchUser = (username) => dispatch => {
fetch(`${url}/${username}`, {credentials: 'include'})
.then(handleErrors)
.then(res => res.json())
.then(res =>
dispatch({
type: FETCH_USER,
payload: res
})
)
.catch(error => console.error('Error:', error))
}
reducers.js
export default function(state = initialState, action) {
switch (action.type) {
case FETCH_USER:
return {
...state,
user: action.payload.user
};
case LIKE:
return {
...state,
user: {
...state.user,
meta: {
...state.user.meta,
like: action.payload.like
}
}
};
store.js
const initialState = {};
const middleware = [thunk];
const store = createStore(
rootReducer,
initialState,
compose(
applyMiddleware(...middleware),
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
)
);
export default store;
console.log of like in Profile.js
const Profile = (props) => {
const { user, like } = props
console.log(like)
ƒ like(username) {
return function (dispatch) {
fetch(url + '/like/' + username, { credentials: 'include', method: 'post' }).then(handleErrors).then(function (res) {
return res.json();
…
If I were to create a normal function such as
const test = () => { console.log('test') }
and change the onClick={} in Profile.js to use that, it works fine.
You should create handler in the User component, call your action creator there and pass it as a callback to child Profile component.
So, your code will looks like:
import { like } from '../../actions/userActions';
class User extends React.Component {
...
onClickHandler = username => {
return () => {
this.props.like(username);
}
}
render() {
const { user } = this.props;
return <Profile user={user} onClickHandler={this.onClickHandler} />
}
}
const mapStateToProps = state => ({
user: state.store.user
});
export default connect(mapStateToProps, {fetchUser, like})(User);
Then, call onClickHandler in your Profile component:
const Profile = props => {
const { onClickHandler, user } = props;
return (
<button onClick={onClickHandler(user.username)}>Like</button>
)
}
Hope it will helps.
I have an API endpoint that returns a list of users in an 'application/stream+json' type response. The items are separated by a new line character.
Example data can be seen here.
Component
class UserList extends Component {
componentDidMount() {
const { fetchUsers } = this.props;
fetchUsers();
}
render() {
const { isFetching = false, users = [] } = this.props;
if (isFetching) {
return <Loader message="Users are loading..." />;
}
if (!users || users.length === 0) {
return 'No users found.';
}
const children = users
.map(user => <UserListItem key={user.id} user={user} />);
return (
<div className="UserList">
<Paper>
<List>
<Subheader>Users</Subheader>
{children}
</List>
</Paper>
</div>
);
}
}
UserList.propTypes = {
users: PropTypes.arrayOf(PropTypes.any),
isFetching: PropTypes.bool.isRequired,
fetchUsers: PropTypes.func.isRequired,
};
UserList.defaultProps = {
users: [],
};
function mapStateToProps(state) {
const { users, isFetching } = state.users;
return {
users,
isFetching,
};
}
function mapDispatchToProps(dispatch) {
return {
fetchUsers: bindActionCreators(actions.fetchUsers, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(UserList);
Reducer
const initialState = {
users: [],
isFetching: false,
};
function fetchUsers(state) {
return {
...state,
isFetching: true,
};
}
function fetchUsersItemReceived(state, action) {
const { user } = action;
return {
...state,
users: [...state.users, user],
isFetching: false,
};
}
export default function (state = initialState, action) {
switch (action.type) {
case actionTypes.FETCH_USERS_REQUEST:
return fetchUsers(state);
case actionTypes.FETCH_USERS_ITEM_RECEIVED:
return fetchUsersItemReceived(state, action);
default:
return state;
}
}
Action (the parser is the Streaming JSON Parser found here)
export function fetchUsers() {
return {
type: actionTypes.FETCH_USERS_REQUEST,
};
}
function fetchUsersItemReceived(user) {
return {
type: actionTypes.FETCH_USERS_ITEM_RECEIVED,
user,
};
}
function fetchUsersSuccess() {
return {
type: actionTypes.FETCH_USERS_SUCCESS,
};
}
function fetchUsersFailure(error) {
return {
type: actionTypes.FETCH_USERS_FAILURE,
error,
};
}
function getJsonStream(url) {
const emitter = new Subject();
const req$ = RxHR
.get(url)
.flatMap(resp => resp.body)
.subscribe(
(data) => {
parser.write(data);
parser.onValue = (value) => {
if (!parser.key) {
emitter.next(value);
}
};
},
err => emitter.error(err),
() => emitter.complete(),
);
return emitter;
}
export const fetchUsersEpic = action$ =>
action$.ofType(actionTypes.FETCH_USERS_REQUEST)
.concatMap(() => getJsonStream(`${api.API_BASE_URL}/user`))
.map(user => fetchUsersItemReceived(user));
configureStore.js
const logger = createLogger();
const epicMiddleware = createEpicMiddleware(rootEpic);
const createStoreWithMiddleware = applyMiddleware(epicMiddleware, logger)(createStore);
export default function configureStore(initialState) {
return createStoreWithMiddleware(rootReducer, initialState);
}
While the list component should be refreshed after EACH item is received, it is refreshed AFTER the whole list is received. Can someone point me to the blocking point in the code?
This isn't a solution to your particular issue (unless by accident lol) but I think a custom Observable is a better fit in this situation instead of a Subject. You can also hook into the Parser's error callback too.
Figured that others searching for streaming JSON with rxjs later might find this handy (untested)
function streamingJsonParse(data$) {
return new Observable((observer) => {
const parser = new Parser();
parser.onError = (err) => observer.error(err);
parser.onValue = (value) => {
if (!parser.key) {
observer.next(value);
}
};
// return the subscription so it's correctly
// unsubscribed for us
return data$
.subscribe({
next: (data) => parser.write(data),
error: (e) => observer.error(e),
complete: () => observer.complete()
});
});
}
function getJsonStream(url) {
return RxHR
.get(url)
.mergeMap(resp => streamingJsonParse(resp.body));
}
When you've had a chance to put together that jsbin let me know!
Turns out the problem was with the jsonParse lib. Switching to oboe.js
fixed it. Using the "!" node selector to select multiple root JSON elements i was able to transform the character stream to a user object stream.
Action
function getJsonStream(url) {
const emitter = new Subject();
const emitter = new Subject();
oboe(url)
.node('!', (item) => {
emitter.next(item);
})
.fail((error) => {
emitter.error(error);
});
return emitter;
}
export const fetchUsersEpic = action$ =>
action$.ofType(actionTypes.FETCH_USERS_REQUEST)
.switchMap(() => getJsonStream(`${api.API_BASE_URL}/user`))
.map(user => fetchUsersItemReceived(user));