Reusable React component with same actions & reducers - reactjs

I want to reuse a react component and share common actions & reducers. My app dashboard has 3 Lists, where each List is fetched with different query param.
All 3 List components have the same props because all 3 of them are being re-rendered once I receive props from reducer.
Is there an dynamic way to display Lists based on query parameter? What I was thinking is to call different reducer in the action file based on the query param. Is there a better way?
Dashboard.js
const Dashboard = () => {
return(
<div>
<List query={QUERY1} />
<List query={QUERY2} />
<List query={QUERY3} />
</div>
)
}
List.js
class List extends Component {
constructor(props) {
super(props);
this.state = {
items: []
};
}
componentWillMount() {
const { query } = this.props;
this.props.onLoad(query);
}
componentWillReceiveProps() {
const { items } = this.props;
this.setState({ items });
}
render() {
return (
<div>
{
this.state.items.map((item, index) =>
<Item data={item} key={index}/>
)
}
</div>
)
}
}
function mapStateToProps(state) {
const { items } = state.item;
return {
items
}
}
function mapDispatchToProps(dispatch) {
return {
onLoad: bindActionCreators(actions.load, dispatch)
}
}
export default connect(mapStateToProps, mapDispatchToProps)(List);
action.js
export function load(query) {
return function (dispatch) {
fetch(`//api.example.com/list?type=${query}&limit=10`)
.then((response) => response.json())
.then((data) => {
dispatch(setItems(data));
});
};
}
reducer.js
export default function(state = [], action) {
switch (action.type) {
case actionTypes.ITEMS_SET:
return setItems(state, action);
}
return state;
}
function setItems(state, action) {
const { items } = action;
return { ...state, items };
}

Note I am a contributor on redux-subpace
redux-subspace came around to solve this problem of having the same component displayed on the page, without crossing over the store values.
It has a feature called namespacing that will allow you to isolate your load actions and components from each other.
const Dashboard = () => {
return(
<div>
<SubspaceProvider mapState={state => state.list1}, namespace='list1'>
<List query={QUERY1} />
</SubspaceProvider>
<SubspaceProvider mapState={state => state.list2}, namespace='list'>
<List query={QUERY2} />
</SubspaceProvider>
<SubspaceProvider mapState={state => state.list3}, namespace='list3'>
<List query={QUERY3} />
</SubspaceProvider>
</div>
)
}
You'll also need to namespace your reducers, you can see how to do that here.

Related

Sorting Data in Reducer Actions

I am trying to create a sort button which when clicked will sort me menu cards alphabetically. My question is how should I have the sort function coded in the Reducer and Actions? I added pseudo-code for sorting in the Reducer as well. When I click the button I am getting "(TypeError): state.slice is not a function".
Edit:
Added my button component and main Container.
Actions:
export const sortMenus = () => {
return dispatch => {
dispatch({ type: "LOADING_MENUS" });
fetch(`/api/menus`)
.then(res => res.json())
.then(responseJSON => {
dispatch({ type: "SORT_MENUS", cards: responseJSON });
});
};
};
Reducer:
export default function MenusReducer(
state = {
cards: [],
loading: false
},
action
) {
switch (action.type) {
case "LOADING_MENUS":
return {
...state
};
case "ADD_MENUS":
return {
...state,
cards: action.cards
};
case "SORT_MENUS":
return state.slice().sort(function(menu1, menu2) {
if (menu1.name < menu2.name) return -1;
if (menu1.name < menu2.name) return 1;
return 0;
});
default:
return state;
}
}
Button Component:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { sortMenus } from ".././actions/dataActions";
import Row from "react-bootstrap/Row";
import Container from "react-bootstrap/Container";
class SortButton extends Component {
constructor() {
super();
this.state = { menus: [] };
}
handleMenuSort = e => {
this.props.sortMenus()
};
render() {
return (
<Container>
<Row>
<div>
<button id="sort-button" title="Sort Menus" onClick= {this.handleMenuSort}>Sort Menus</button>
</div>
</Row>
</Container>
)
}
}
const mapStateToProps = state => {
return {
menus: state.menus
}
};
const mapDispatchToProps = dispatch => {
return {
sortMenus: params => dispatch(sortMenus(params)),
}
};
export default connect(mapStateToProps, mapDispatchToProps)(SortButton)
Container:
class MainContainer extends Component {
displayCards = () => {
switch(this.props.path) {
case "menus":
return (this.props.menus.cards.map(card => (
<NavLink style={{ color: "black" }} to={`/menus/${card.id}`} key={card.id}><MenuCard view={this.props.displayObject} info={card} /></NavLink>
)));
default:
return (<div>Empty</div>)
}
};
render() {
return (
<CardColumns>
{this.displayCards()}
</CardColumns>
)
}
}
const mapStateToProps = state => {
return {
menus: state.menus
}
};
const mapDispatchToProps = dispatch => {
return {
displayObject: (id, category, type) => dispatch(displayObject(id, category, type)),
}
};
export default connect(mapStateToProps, mapDispatchToProps)(MainContainer)
Your state is an object, not an array. You likely mean to sort the stored cards array.
state.cards.slice(... instead of state.slice(...
case "SORT_MENUS":
return state.cards.slice().sort(function(menu1, menu2) {
if (menu1.name < menu2.name) return -1;
if (menu1.name < menu2.name) return 1;
return 0;
});
Side note: You may also want to clear/set your loading state upon successful data fetching. ;)
EDIT
You are mapping undefined state within mapStateToProps, then mapping over it in the component. Change mapStateToProps to access the correct defined property.
const mapStateToProps = state => ({
cards: state.cards,
});
Then you can iterate over the new cards prop.
case "menus":
return (this.props.cards.map(card => (
<NavLink
style={{ color: "black" }}
to={`/menus/${card.id}`}
key={card.id}
>
<MenuCard view={this.props.displayObject} info={card} />
</NavLink>
)));
You can simply store the fetched menu in application state.
You can have standalone action say SORT_MENU_BY_ALPHABET.
You can simply dispatch this action on button handler as well as on Ajax success. this dispatch may not have any payload associated.
hope it helps.
in reducer you defined state as object and you're trying to do array operation on it. state.slice().
slice is a function available for arrays. so its throwing error.
you should be doing
state.cards.slice().sort((a,b)=> a-b)

Understanding react js constructor in children component

I would like to understand the behavior of react component constructor. Let suppose I have three components - PageComponent, ListComponent, ItemComponent. My pseudo-code structure is:
PageComponent (get data from redux, fetch data)
ListComponent (obtains data as props, in loop (map) renders list of ItemComponents)
ItemComponent (obtains item data as props, renders item, manipulate data)
Logic:
- when data in ItemComponent changes, changes are stored in REDUX and this change caused list re-rendering.
Use-case 1:
- PageComponent renders ListComponent and ListComponent renders list of ItemComponets
- when REDUX listItem data chages, PageComponent is updated, ListComponent is updated and ItemComponent CONSTRUCTOR is called (its local state is reset)
Use-case 2:
- PageComponent renders only LIST (using map loop) of ItemComponents.
- when REDUX listItem data chages, PageComponent is updated ItemComponent CONSTRUCTOR is NOT called (component is "only" updated) (and its local state is NOT reset)
Why there is a different behavior in these examples?
Source code:
PageComponent:
import React from 'react'
...
class UsersPage extends React.Component {
constructor(props) {
super(props)
props.actions.getUsers();
}
render() {
const {users} = this.props
return (
<Main>
{/* // NO ITEM CONSTRUCTOR IS CALLED
users.data.items.map((item, index) => {
return <ListItemComponent
data={item}
itemMethods={{
getItem: (data) => this.props.actions.getUser(data),
onEdit: (data) => this.props.actions.updateUser(data),
onDelete: (data) => this.props.actions.deleteUser(data),
validation: (data) => validateInput(this.props.strings, data)
}}
key={index}
/>
})*/
}
{ // ITEM CONSTRUCTOR IS CALLED
<ListComponent
loading={users.isFetching}
data={users.data}
methods={{
getItem: (data) => this.props.actions.getUser(data),
onEdit: (data) => this.props.actions.updateUser(data),
onDelete: (data) => this.props.actions.deleteUser(data),
validation: (data) => validateInput(this.props.strings, data)
}}
/>}
</Main>
);
}
}
UsersPage.propTypes = {
users: PropTypes.object.isRequired,
strings: PropTypes.object.isRequired,
}
function mapStateToProps(state) {
return {
users: state.users,
strings: state.strings.data || {},
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
getUsers,
getUser,
addUser,
updateUser,
deleteUser,
}, dispatch)
};
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(withAlert(UsersPage));
ListComponent:
import React from 'react'
...
class ListComponent extends React.Component {
getList() {
return <div className="list-outer">
<Row>
{
items.map((item, index) => {
return <ListItemComponent
data={item}
itemMethods={methods}
key={index}
/>
})
}
</Row>
</div>
}
render() {
const {loading} = this.props
return (
<div className="list-wrapper">
{
loading ? <Spinner visible={true}/>
:
this.getList()
}
</div>
)
}
}
ListComponent.propTypes = {
loading: PropTypes.bool.isRequired,
data: PropTypes.object.isRequired,
methods: PropTypes.object.isRequired,
}
export default ListComponent
ListItemComponent:
import React from 'react'
...
class ListItemComponent extends React.Component {
constructor(props) {
super(props)
this.state = {
editMode: false,
}
}
toggleEditMode(){
const editMode = this.state.editMode
this.setState({editMode: !editMode})
}
onEdit(id) {
const itemMethods = this.props.itemMethods
this.toggleEditMode()
itemMethods.getItem({id: id})
}
onDelete(item) {
//...
}
getFields(rowData, index) {
return <div key={index}>
{
rowData.map((itm, idx) => {
return <div key={idx}>{itm.label}: {itm.value}</div>
})
}
</div>
}
render() {
const editMode = this.state.editMode
const {data, itemMethods, strings} = this.props
return (
editMode ?
<Form
id={data.id}
onSubmit={(data) => itemMethods.onEdit(data)}
validation={(data) => itemMethods.validation(data)}
onCloseForm={() => this.toggleEditMode()}
/>
:
<Col xs={12}>
<div>
<div
{this.getFields(data)}
</div>
<div className="controls">
<button
className="btn btn-theme inverse danger"
onClick={() => this.onDelete(data)}
>{strings.delete}</button>
<button
onClick={() => this.onEdit(data.id)}
className="btn btn-theme" type="button"
>
{strings.edit}
</button>
</div>
</div>
</Col>
)
}
}
ListItemComponent .propTypes = {
strings: PropTypes.object.isRequired,
data: PropTypes.object.isRequired,
itemMethods: PropTypes.object.isRequired,
}
function mapStateToProps(state) {
return {
strings: state.strings.data || {}
};
}
export default connect(
mapStateToProps,
null,
)(ListItemComponent )
Ensure each ItemComponent has a key prop set. When React renders your list of items, it needs to know how to identify each element and React leaves it up to you to do this. If you omit the key prop, React will destroy and re-create your list upon each re-render, which means calling the component constructor.
If you provide the exact code you're using, we can better point out where your issue is coming from.
You can read more about lists and keys here.
SOLVED
It was cause by ListComponent and the loading prop that was placed as condion in render function. When item was edited, prop loading was set to true, spinner became visible AND it was the only element in ListComponent and therefore the list items were unmounted

Firebase/React/Redux Component has weird updating behavior, state should be ok

I am having a chat web app which is connected to firebase.
When I refresh the page the lastMessage is loaded (as the gif shows), however, for some reason, if the component is otherwise mounted the lastMessage sometimes flickers and disappears afterwards like it is overridden. When I hover over it, and hence update the component, the lastMessage is there.
This is a weird behavior and I spent now days trying different things.
I would be very grateful if someone could take a look as I am really stuck here.
The db setup is that on firestore the chat collection has a sub-collection messages.
App.js
// render property doesn't re-mount the MainContainer on navigation
const MainRoute = ({ component: Component, ...rest }) => (
<Route
{...rest}
render={props => (
<MainContainer>
<Component {...props} />
</MainContainer>
)}
/>
);
render() {
return (
...
<MainRoute
path="/chats/one_to_one"
exact
component={OneToOneChatContainer}
/>
// on refresh the firebase user info is retrieved again
class MainContainer extends Component {
componentDidMount() {
const { user, getUserInfo, firebaseAuthRefresh } = this.props;
const { isAuthenticated } = user;
if (isAuthenticated) {
getUserInfo(user.id);
firebaseAuthRefresh();
} else {
history.push("/sign_in");
}
}
render() {
return (
<div>
<Navigation {...this.props} />
<Main {...this.props} />
</div>
);
}
}
Action
// if I set a timeout around fetchResidentsForChat this delay will make the lastMessage appear...so I must have screwed up the state / updating somewhere.
const firebaseAuthRefresh = () => dispatch => {
firebaseApp.auth().onAuthStateChanged(user => {
if (user) {
localStorage.setItem("firebaseUid", user.uid);
dispatch(setFirebaseAuthUser({uid: user.uid, email: user.email}))
dispatch(fetchAllFirebaseData(user.projectId));
}
});
};
export const fetchAllFirebaseData = projectId => dispatch => {
const userId = localStorage.getItem("firebaseId");
if (userId) {
dispatch(fetchOneToOneChat(userId));
}
if (projectId) {
// setTimeout(() => {
dispatch(fetchResidentsForChat(projectId));
// }, 100);
...
export const fetchOneToOneChat = userId => dispatch => {
dispatch(requestOneToOneChat());
database
.collection("chat")
.where("userId", "==", userId)
.orderBy("updated_at", "desc")
.onSnapshot(querySnapshot => {
let oneToOne = [];
querySnapshot.forEach(doc => {
let messages = [];
doc.ref
.collection("messages")
.orderBy("created_at")
.onSnapshot(snapshot => {
snapshot.forEach(message => {
messages.push({ id: message.id, ...message.data() });
});
});
oneToOne.push(Object.assign({}, doc.data(), { messages: messages }));
});
dispatch(fetchOneToOneSuccess(oneToOne));
});
};
Reducer
const initialState = {
residents: [],
oneToOne: []
};
function firebaseChat(state = initialState, action) {
switch (action.type) {
case FETCH_RESIDENT_SUCCESS:
return {
...state,
residents: action.payload,
isLoading: false
};
case FETCH_ONE_TO_ONE_CHAT_SUCCESS:
return {
...state,
oneToOne: action.payload,
isLoading: false
};
...
Main.js
// ...
render() {
return (...
<div>{React.cloneElement(children, this.props)}</div>
)
}
OneToOne Chat Container
// without firebaseAuthRefresh I don't get any chat displayed. Actually I thought having it inside MainContainer would be sufficient and subscribe here only to the chat data with fetchOneToOneChat.
// Maybe someone has a better idea or point me in another direction.
class OneToOneChatContainer extends Component {
componentDidMount() {
const { firebaseAuthRefresh, firebaseData, fetchOneToOneChat } = this.props;
const { user } = firebaseData;
firebaseAuthRefresh();
fetchOneToOneChat(user.id || localStorage.getItem("firebaseId"));
}
render() {
return (
<OneToOneChat {...this.props} />
);
}
}
export default class OneToOneChat extends Component {
render() {
<MessageNavigation
firebaseChat={firebaseChat}
firebaseData={firebaseData}
residents={firebaseChat.residents}
onClick={this.selectUser}
selectedUserId={selectedUser && selectedUser.residentId}
/>
}
}
export default class MessageNavigation extends Component {
render() {
const {
onClick,
selectedUserId,
firebaseChat,
firebaseData
} = this.props;
<RenderResidentsChatNavigation
searchChat={this.searchChat}
residents={residents}
onClick={onClick}
firebaseData={firebaseData}
firebaseChat={firebaseChat}
selectedUserId={selectedUserId}
/>
}
}
const RenderResidentsChatNavigation = ({
residents,
searchChat,
selectedUserId,
onClick,
firebaseData,
firebaseChat
}) => (
<div>
{firebaseChat.oneToOne.map(chat => {
const user = residents.find(
resident => chat.residentId === resident.residentId
);
const selected = selectedUserId == chat.residentId;
if (!!user) {
return (
<MessageNavigationItem
id={chat.residentId}
key={chat.residentId}
chat={chat}
onClick={onClick}
selected={selected}
user={user}
firebaseData={firebaseData}
/>
);
}
})}
{residents.map(user => {
const selected = selectedUserId == user.residentId;
const chat = firebaseChat.oneToOne.find(
chat => chat.residentId === user.residentId
);
if (_isEmpty(chat)) {
return (
<MessageNavigationItem
id={user.residentId}
key={user.residentId}
chat={chat}
onClick={onClick}
selected={selected}
user={user}
firebaseData={firebaseData}
/>
);
}
})}
</div>
}
}
And lastly the item where the lastMessage is actually displayed
export default class MessageNavigationItem extends Component {
render() {
const { hovered } = this.state;
const { user, selected, chat, isGroupChat, group, id } = this.props;
const { messages } = chat;
const item = isGroupChat ? group : user;
const lastMessage = _last(messages);
return (
<div>
{`${user.firstName} (${user.unit})`}
{lastMessage && lastMessage.content}
</div>
)
}
In the end it was an async setup issue.
In the action 'messages' are a sub-collection of the collection 'chats'.
To retrieve them it is an async operation.
When I returned a Promise for the messages of each chat and awaited for it before I run the success dispatch function, the messages are shown as expected.

React updates the state but not the component with Redux

I have a blog application that I want to sort my posts by votes or title, so I have this buttons for sorting:
<Button size='mini' onClick={() => {this.props.sortByVotes()}}>
Votes
</Button>
<Button size='mini' onClick={() => {this.props.sortByTitle()}}>
Title
</Button>
The actions is like this:
export const sortByVotes = posts => ({ type: SORT_BY_VOTES })
export const sortByTitle = posts => ({ type: SORT_BY_TITLE })
And reducer is as it follows:
case SORT_BY_VOTES:
return {
...state,
posts: state.posts.sort((a, b) => b.voteScore - a.voteScore)
}
case SORT_BY_TITLE:
return {
...state,
posts: state.posts.sort((a, b) => {
if (a.title > b.title) return 1
if (a.title < b.title)return -1
return 0
})
}
Finally, in the Main.js view I get posts in componentDidMount and shows it like this:
<Item.Group divided>
{this.props.posts.map((p, idx) =>
<PostSmall key={idx}
id={p.id}
title={p.title}
body={p.body}
category={p.category}
voteScore={p.voteScore}
/>
)}
</Item.Group>
Still in Main.js, I map the posts from state like this:
function mapStateToProps(state) {
return {
posts: state.posts.posts,
categories: state.categories.categories
}
}
As you can see, nothing special here.
The problem is: the state is update as expected, but view not.
Ti'll now I have not figured out how to solve it, and why this is happening.
Any help will be grateful.
The object posts is not changing hence React is not rendering the component.
I have added a work around fix.
reducers/posts.js
case SORT_BY_VOTES:
const posts = Object.assign({},{posts:state.posts.sort((a, b) => b.voteScore - a.voteScore
)})
return Object.assign ({}, state, posts);
In Main.js
import React from 'react'
import { connect } from 'react-redux'
import PropTypes from 'prop-types'
import { Container, Divider, Grid, Item } from 'semantic-ui-react'
import Categories from '../components/Categories'
import PostSmall from '../components/PostSmall'
import PostsSorter from '../components/PostsSorter'
import { fetchPosts } from '../actions/posts'
import { fetchCategories } from '../actions/categories'
class Main extends React.Component {
render() {
const posts = this.props.posts ? this.props.posts.posts || [] : [];
console.log('render')
return (
<Container>
<Grid columns='equal'>
<Grid.Column>
<PostsSorter/>
<Divider/>
<Categories categories={this.props.categories}/>
</Grid.Column>
<Grid.Column width={10}>
<Item.Group divided>
{posts.map((p, idx) =>
<PostSmall key={idx} post={p}/>
)}
</Item.Group>
</Grid.Column>
</Grid>
</Container>
);
}
componentDidMount() {
this.props.getPosts()
this.props.getCategories()
}
}
Main.propTypes = {
posts: PropTypes.array,
categories: PropTypes.array
}
Main.defaultProps = {
posts: [],
categories: []
}
function mapStateToProps(state) {
console.log(state);
return {
posts: state.posts,
categories: state.categories.categories
}
}
function mapDispatchToProps(dispatch) {
return {
getPosts: () => dispatch(fetchPosts()),
getCategories: () => dispatch(fetchCategories())
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Main)
But you have to refactor the code in order to make the component pure and connect the redux state only to view.
Your action doesnt send a payload. Are you initiating an API call in actions to send to the reducer? In your reducer, you need to capture the payload from actions and then update the state.
Actions: actions.tsx
Reducers: reducers.tsx
So, the common workflow is to have a parameter in actions and then modify the default state in the reducer. See a sample above for reference.
Hope this helps.

react redux not changes component after store change

I'm stating to learn react and redux so i think there are many things that i don't know.
I have a problem with missed re-rendering component on store changes.
This is my project structure: https://i.stack.imgur.com/tJJSg.png
And here is my code:
App.js:
class App extends Component {
render() {
return (
<div className="App">
<Nav sortByDate={()=>{this.props.sortBy(SORT_BY_DATE)}} sortByLikes={()=>{this.props.sortBy(SORT_BY_LIKES)}} />
<Items comments={this.props.comments} getList={()=>{this.props.sortBy(GET_LIST)}}/>
</div>
);
}
}
const mapStateToProps = (state) => {
return {
comments: state.comments
};
};
const mapDispatchToProps = (dispatch) => {
return {
sortBy: (action) => {
dispatch(sortBy(action));
}
};
};
export default connect (mapStateToProps, mapDispatchToProps) (App);
CommentList.js:
class ListItems extends Component {
constructor(props){
super(props);
this.state = {
comments: props.comments
};
}
componentWillMount() {
this.props.getList();
}
componentWillReceiveProps(nextProps) {
if (this.props.comments !== nextProps.comments) {
this.setState({
comments: nextProps.comments
});
}
}
getComments() {
return (this.state.comments.map(function (object) {
return <Item numLikes={object.num_like} id={object.id} comment={object.comment} date={object.date}
sender={object.sender}/>
}));
}
render() {
return (
<Container>
<Row>
<Col lg={2} md={1} xs={0}/>
<Col lg={8} md={10} xs={12}>
{this.getComments()}
</Col>
<Col lg={2} md={16} xs={0}/>
</Row>
</Container>
);
}
}
export default ListItems;
Reducers.js:
const listReducer = (state = {comments: []}, action) => {
function toDate(dateStr) {
const [day, month, year] = dateStr.split("/")
return new Date(year, month - 1, day)
}
function commentSortedByDate(comments) {
const sorted = comments.sort(function(a, b) {
return toDate(b.date) - toDate(a.date);
})
return sorted;
}
function commentSortedByLikes(comments) {
const sorted = comments.sort(function(a, b) {
return parseInt(b.num_like) - parseInt(a.num_like);
})
return sorted;
}
switch (action.type) {
case SORT_BY_DATE:
console.log("sort by date");
state={
comments: commentSortedByDate(state.comments)
}
break;
case SORT_BY_LIKES:
console.log("sort by likes");
state={
comments: commentSortedByLikes(state.comments)
}
break;
case GET_LIST:
state = {
comments: action.payload
}
break;
}
return state;
};
export default listReducer;
The problem is certainly with this two components.
I have 3 actions:
GET_LIST (in a middleware call a rest service getting the json of comments and update the store).
SORT_BY_DATE (in the reducer sort the array of comments by date and update the store).
SORT_BY_LIKES (same).
The comments in the store are effectively sorted.
First of all the app dispatch automatically the GET_LIST action and it works, pass the props with comments correctly to the CommentList.js component and successfully render the list of CommentItem.
Now the problem:
The click of a button in the Navbar component will dispatch a SORT_BY action that updates the store and finally calls the MapStateToProps function in App, but this time the CommentList stay the same and componentWillReceiveProps is not called.
Why? Can anyone help me?
You are mutating the state (sort function) instead of creating a new Array in your reducer. This prevents the component from re-rendering as it is not notified of a change. To fix it you could make your functions pure:
function commentSortedByDate(comments) {
const copy = [...comments];
copy.sort(function(a, b) {
return toDate(b.date) - toDate(a.date);
})
return copy;
}
function commentSortedByLikes(comments) {
const copy = [...comments];
copy.sort(function(a, b) {
return parseInt(b.num_like) - parseInt(a.num_like);
})
return copy;
}
This way you return a new array instead of old one (with sorted elements).

Resources