I've been working on a React app and have gotten to a point where I'll need Redux to handle some aspects of it.
After reading a bunch of tutorials, I'm fairly stuck on how to make my "smarter" components "dumber" and move functions into my actions and reducers.
So, for example, one aspect of the app is more of a to-do list style.
One of my classes starts like this:
export default class ItemList extends React.Component {
constructor() {
super();
this.state = { items: [],
completed: [],
};
this.addItem = this.addItem.bind(this);
this.completeItem = this.completeItem.bind(this);
this.deleteItem = this.deleteItem.bind(this);
}
addItem(e) {
var i = this.state.items;
i.push({
text: this._inputElement.value,
paused: false,
key: Date.now()
});
this.setState({ items: i });
e.preventDefault();
this._inputElement.value = '';
this._inputElement.focus();
}
completeItem(e) {
this.deleteItem(e);
var c = this.state.completed;
c.push({
text: e.target.parentNode.parentNode.getElementsByClassName('item-name')[0].innerHTML,
paused: false,
key: Date.now()
});
this.setState({ completed: c });
}
deleteItem(e) {
var i = this.state.items;
var result = i.filter(function(obj) {
return obj.text !== e.target.parentNode.parentNode.getElementsByClassName('item-name')[0].innerHTML;
});
this.setState({ items: result });
}
// ... more irrelevant code here ...
// there's a function called createTasks that renders individual items
render() {
var listItems = this.state.items.map(this.createTasks);
return <div className="item-list">
<form className="form" onSubmit={this.addItem}>
<input ref={(a) => this._inputElement = a}
placeholder="Add new item"
autoFocus />
<button type="submit"></button>
</form>
{listItems}
</div>;
}
}
So, as you can see, it's very logic-heavy. I've started adding Redux by adding a <Provider> in my index file, and made a basic reducers file that is fairly empty so far:
import { combineReducers } from 'redux';
const itemList = (state = {}, action) => {
};
// ... other irrelevant reducers
const rootReducer = combineReducers({
itemList,
// ...
});
export default rootReducer;
...and I've made an actions file that doesn't have much in it yet either.
I've been struggling to figure out:
Most actions I've seen examples of just return some kind of JSON, what do I return in the reducer that uses that JSON that my component can use?
How much of my component logic is reusable, or should I just forget it? What is the best way to go about this to reuse as much code as I've written as possible?
First of all you need to understand the overall picture of how redux works with react.
Before coming to that lets first understand what are smart components and dumb components.
Smart Components
All your code logic needs to be handled here
They are also called containers.
They interact with the store(aka state management) to update your components.
Dumb Components
They just read props from your containers and render you components
This is just the UI view and should not contain any logic.
All styling/html/css comes in your dumb components.
Here is an amazing piece of article which you can go through to understand smart and dumb components if you still have doubts.
Ok, now lets try understanding how redux works:-
Your smart components(aka containers) interact with your redux store
You fire actions from your containers.
Your actions call your apis
The result of your action updates the store through a reducer
You containers read the store through mapStateToProps function and as soon as value in store changes it updates your component.
Now lets consider your todo example
TodoListContainer.js
class TodoListContainer extends Component {
componentWillMount () {
// fire you action action
}
render () {
return (
<Todos todos=={this.props.todos} />
)
}
}
function mapStateToProps(state) {
const {todos} = state;
return {
todos;
}
}
export default connect(mapStateToProps)(TodoListContainer)
TodoList.js
class TodoList extends Component {
renderTodos() {
return this.props.todos.map((todo)=>{
return <Todo todo={todo} key={todo.id} />
})
}
render () {
return () {
if (this.props.todos.length === 0) {
return <div>No todos</div>
}
return (
<div>
{this.renderTodos()}
</div>
)
}
}
}
export default class TodoList
Todo.js
class Todo extends Component {
render () {
return (
<div>
<span>{this.props.todo.id}</span>
<span>{this.props.todo.name}</span>
</div>
)
}
}
Reducer
export default function todos(state={},action) {
switch (action.type) {
case 'RECEIVE_TODOS':
return Object.assign(state,action.todos);
}
}
action
function fetchTodos() {
return(dispatch) => {
axios.get({
//api details
})
.then((res)=>{
dispatch(receiveTodos(res.todos))
})
.catch((err)=>{
console.warn(err)
})
}
}
function receiveTodos(todos) {
return {
type: 'RECEIVE_TODOS',
todos
}
}
Now if you have read redux documentation you would see that actions return objects then how would i call my api there which returns a function instead of an object. For that I used redux thunk about which you can read here.
I gave you an example in which you can fetch todos. If you want to do other operations like deleteTodo, addTodo, modifyTodo then you can do that in appropriate components.
DeleteTodo - you can do in TodoListContainer.
AddingTodo - you can do in TodoListContainer.
Changing State(completed/Pending) - you can do in TodoListContainer.
ModifyingTodo - you can do in TodoContainer.
You can also check out here for a detailed example, but before that I would say just should go through basics of redux which you can find here
P.S: I wrote code on the fly so it might not work properly but it should work with little modification.
Related
The problem:
I want to have simple boolean flag that will be true when modal is opened and false when it is closed. And I want to update other components reactively depends on that flag
I hope there is a way to do it with relay only (Apollo has a solution for that). I don't want to connect redux of mobx or something like that (It is just simple boolean flag!).
What I already have:
It is possible to use commitLocalUpdate in order to modify your state.
Indeed I was able to create and modify my new flag like that:
class ModalComponent extends PureComponent {
componentDidMount() {
// Here I either create or update value if it exists
commitLocalUpdate(environment, (store) => {
if (!store.get('isModalOpened')) {
store.create('isModalOpened', 'Boolean').setValue(true);
} else {
store.get('isModalOpened').setValue(true);
}
});
}
componentWillUnmount() {
// Here I mark flag as false
commitLocalUpdate(environment, (store) => {
store.get('isModalOpened').setValue(false);
});
}
render() {
// This is just react component so you have full picture
return ReactDOM.createPortal(
<div
className={ styles.modalContainer }
>
dummy modal
</div>,
document.getElementById('modal'),
);
}
}
The challenge:
How to update other components reactively depends on that flag?
I can't fetch my flag like this:
const MyComponent = (props) => {
return (
<QueryRenderer
environment={ environment }
query={ graphql`
query MyComponentQuery {
isModalOpened
}`
} //PROBLEM IS HERE GraphQLParser: Unknown field `isModalOpened` on type `Query`
render={ ({ error, props: data, retry }) => {
return (
<div>
{data.isModalOpened}
<div/>
);
} }
/>);
};
Because Relay compiler throws me an error: GraphQLParser: Unknown field 'isModalOpened' on type 'Query'.
And the last problem:
How to avoid server request?
That information is stored on client side so there is no need for request.
I know there a few maybe similar questions like that and that. But they doesn't ask most difficult part of reactive update and answers are outdated.
If you need to store just one flag as you said, I recommend you to use React Context instead of Relay. You could do next:
Add Context to App component:
const ModalContext = React.createContext('modal');
export class App extends React.Component {
constructor() {
super();
this.state = {
isModalOpened: false
}
}
toggleModal = (value) => {
this.setState({
isModalOpened: value
})
};
getModalContextValue() {
return {
isModalOpened: this.state.isModalOpened,
toggleModal: this.toggleModal
}
}
render() {
return (
<ModalContext.Provider value={this.getModalContextValue()}>
//your child components
</ModalContext.Provider>
)
}
}
Get value from context everywhere you want:
const MyComponent = (props) => {
const { isModalOpened } = useContext(ModalContext);
return (
<div>
{isModalOpened}
</div>
);
};
If you will use this solution you will get rid of using additional libraries such as Relay and server requests.
I am quite new to development with React and I am currently trying to get my head around some basic react and redux things. Unfortunately I am experiencing an issue which I cannot fix on my own.
I have written a mock-api which returns players (profileUrl, username, realname, id). I am dispatching an action which successfully gets me one of those players and I can also pass it to my components props using redux' mapStateToPropsfunction. But I cannot render any of that data in my render function. The react devtools even show me that the single player is getting returned as an array.
The component:
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as playerActions from '../../actions/playerActions';
class SinglePlayer extends React.Component {
componentDidMount() {
this.props.actions.loadPlayer(this.props.match.params.playerid);
}
/**
* Render the component.
*/
render() {
return (
<div>
{ this.props.currentPlayer.username }
</div>
)
}
}
/**
* Defines the state which is exposed to this component.
*
* #param { object } reduxStore The entire redux store.
* #param { object } ownProps The properties which belong to the component.
*/
const mapStateToProps = (reduxStore, ownProps) => {
return {
currentPlayer: reduxStore.playerReducer.currentPlayer
}
}
/**
* Defines which actions are exposed to this component.
*
* #param { function } dispatch This function is used to dispatch actions.
*/
const mapDispatchToProps = (dispatch) => {
return {
actions: bindActionCreators(playerActions, dispatch)
}
}
export default connect(mapStateToProps, mapDispatchToProps)(SinglePlayer);
React DevTools:
Screenshot of React Devtools props
Redux DevTools:
Screenshot of Redux Devtools data
As you can tell from the image above, the currentPlayer props is inside the playerReducer object.
I have also tried looping over the array like so, with no success either. I just get the error-message stating that .map() is not a function.
this.props.currentPlayer.map(function(player, index) {
return <p>{ player.username }</p>
)}
Error when using .map():
TypeError: this.props.currentPlayer.map is not a function
Can someone tell me what I am doing wrong?
You set your current player by params id at componentDidMount . Your render takes place before that currentPlayer is set hence the error. Add a recheck in your render like below.
render() {
return (
<div>
{
this.props.currentPlayer &&
this.props.currentPlayer.map(function(player, index) {
return <p>{ player.username }</>
)}
}
</div>
)
}
Or
render() {
return (
<div>
{
this.props.currentPlayer ?
this.props.currentPlayer.map(function(player, index) {
return <p>{ player.username }</>
)}
:
null
}
</div>
)
}
Either way it should work. That way this.props.currentPlayer will not be rendered or accessed until its available.
Update
Udate your mapStateToProps to
const mapStateToProps = (state) => {
return {
currentPlayer: state.currentPlayer
}
}
I think from your reduxDev tool, currentPlayer is not under any object.
in first render this.props.currentPlayer is empty!
set empty array "currentPlayer" in state and insert insert this.props.currentPlayer in this.state.currentPlayer and render from state
I managed to solve the issue myself now. The posts here kinda inspired me to try some new things. It was my mock-api which returned the data in a strange and unexpected (at least for me it was unexpected) way.
The dataset:
const players = [
{
profileUrl: 'https://profile.url',
username: 'player1',
realname: 'Max Muster',
steamId: 'player1'
},
{
profileUrl: 'https://profile.url',
username: 'player2',
realname: 'Max Mustermann',
steamId: 'player2'
},
{
profileUrl: 'https://profile.url',
username: 'player3',
realname: 'Maxime Musterfrau',
steamId: 'player3'
},
];
The filtermethod used:
var player = players.filter(function(el) {
return el.steamId === 'player1';
});
If you assume an array of multiple players like above, the shown filtermethod extracts the correct object but still keeps it wrapped in an array. That was producing the mistake...
Thanks a lot for the help guys!
I am fetching data in parent 'wrapper' component and pass it down to two child components. One child component receives it well, another does not.
In container:
const mapStateToProps = createStructuredSelector({
visitedCountriesList: getVisitedCountriesList(),
visitedCountriesPolygons: getVisitedCountriesPolygons()
});
export function mapDispatchToProps(dispatch) {
return {
loadVisitedCountries: () => {
dispatch(loadVisitedCountriesRequest())
},
};
}
in redux-saga I fetch data from API and store them:
function mapPageReducer(state = initialState, action) {
switch (action.type) {
case FETCH_VISITED_COUNTRIES_SUCCESS:
return state
.setIn(['visitedCountriesPolygons', 'features'], action.polygons)
}
Selectors:
const getVisitedCountriesList = () => createSelector(
getMapPage,
(mapState) => {
let countriesList = mapState.getIn(['visitedCountriesPolygons', 'features']).map(c => {
return {
alpha3: c.id,
name: c.properties.name
}
});
return countriesList;
}
)
const getVisitedCountriesPolygons = () => createSelector(
getMapPage,
(mapState) => mapState.get('visitedCountriesPolygons')
)
in a wrapper component I render two components, triggering data fetch and passing props down to child components (visitedCountriesPolygons and visitedCountriesList):
class MapView extends React.Component {
constructor(props) {
super(props)
this.props.loadVisitedCountries();
}
render() {
return (
<div>
<Map visitedCountriesPolygons={this.props.visitedCountriesPolygons} />
<MapActionsTab visitedCountriesList={this.props.visitedCountriesList} />
</div>
);
}
}
Then, in first child component Map I receive props well and can build a map:
componentDidMount() {
this.map.on('load', () => {
this.drawVisitedPolygons(this.props.visitedCountriesPolygons);
});
};
But in the second component MapActionsTab props are not received at initial render, but only after any update:
class MapActionsTab extends React.Component {
constructor(props) {
super(props);
}
render() {
let countriesList = this.props.visitedCountriesList.map(country => {
return <li key={country.alpha3}>{country.name}</li>;
}) || '';
return (
<Wrapper>
<div>{countriesList}</div>
</Wrapper>
);
}
}
UPD:
Saga to fetch data form API:
export function* fetchVisitedCountries() {
const countries = yield request
.get('http://...')
.query()
.then((res, err) => {
return res.body;
});
let polygons = [];
yield countries.map(c => {
request
.get(`https://.../${c.toUpperCase()}.geo.json`)
.then((res, err) => {
polygons.push(res.body.features[0]);
})
});
yield put(fetchVisitedCountriesSuccess(polygons));
}
and a simple piece of reducer to store data:
case FETCH_VISITED_COUNTRIES_SUCCESS:
return state
.setIn(['visitedCountriesPolygons', 'features'], action.polygons)
Why is it different and how to solve it, please?
thanks,
Roman
Apparently, this works correct and it was just a minor issue in another place (not pasted here and not errors reported).
After thorough clean up and refactoring it worked as expected.
Conclusion: always keep your code clean, use linter and follow best practices :)
I think the problem may be in your selectors, in particular this one, whose component parts being executed immediately (with no fetched data values), and hence values will not change as it is memoized. This means that it will not cause an update to the component should the the underlying data change from the fetched data
const mapStateToProps = createStructuredSelector({
visitedCountriesList: getVisitedCountriesList, // should not execute ()
visitedCountriesPolygons: getVisitedCountriesPolygons // should not execute ()
});
By not executing the composed selectors immediately, mapStateToProps will call them each time the state changes and they should select the new values and cause an automatic update of your react component
In my top-level component App.js, I have
componentWillMount() {
let data = [];
for (let i = 0; i < 10; i++) {
let todayURL = 'http://www.someURL.com/';
let viewDate = moment()
.add(1, 'years')
.add(i, 'days')
.format('MMM-DD');
let date = moment()
.add(1, 'years')
.add(i, 'days')
.format('YYYY-MMM-DD');
let cardURL =
todayURL +
moment()
.add(1, 'years')
.add(i, 'days')
.format('YYYY/MMM-DD.png');
data.push({
id: i,
date,
uri: cardURL
});
}
store.dispatch({ type: 'data_generated', payload: data });
My reducer is handled this way:
export default (state = [], action) => {
case 'data_generated':
return { ...state, calendarData: action.payload };
default:
return state;
}
};
I combine my reducers:
import { combineReducers } from 'redux';
import CalendarReducer from './CalendarReducer';
export default combineReducers({
calendarData: CalendarReducer
});
The idea behind this is to have an initial dataset that the app can use and persist.
In a child component that displays the information in the dataset I access state:
render() {
console.log(this.props)
return( blah blah);
}
const mapStateToProps = state => {
return { calendarData: state.calendarData };
};
export default connect(mapStateToProps)(HomeScreen);
But when I console log I see an empty array:
This is a React Native app using React Navigation.
What's going on?
After a lengthy chat I've figured out what was wrong. Unfortunately for many of us, React, just like native JavaScript, is very good at letting you place your code improperly.
You were configuring your route within a component and had plenty of logic in the life cycle functions, one of which was dispatching data to the store which was never reaching your reducers.
Your problem can be solved, as proven in our chat, by separating your action code inside your App.js to a dedicated action.js file and then importing and adding the same function to mapDispatchToProps inside your Home.jsx. And finally, on componentWillMount() of Home.jsx you invoke the action by this.props.actionName()
Your code does not seem to follow any conventions recommended by Reacts developers themselves.
If you want to improve your React's programming skills along with your programming skills in general, I recommend you the following Udemy course by Stephen Grider.
Try call the props in the constructor on the component. It's a good debut to debug
Hope it helps
constructor(props) {
super(props);
console.log(this.props);
this.state = {
calendarData: props.calendarData
};
}
componentWillReceiveProps(nextProps) {
if (this.props.calendarData !== nextProps.calendarData) {
this.setState({
calendarData: nextProps.calendarData,
});
}
}
I am new to react and redux and I am facing a very strange issue and it is almost a week that I am trying different ways but no result. I have two component channel and its children. This is how it works: first the channel gets a list of channel from the server and then it in channel component there is a loop which send each channel to the storyboard then in storyboard I call another ajax call to get a list of stories for that specific channel. So here in storyboard as you can see I need to have my reducer separated from channel reducer since when I mix them there will be an infinite loop and browser crashes. Anyway this works on load and even when I use a button just for test and I updated channel reducer with the new channel list and it works perfectly fine, by that I mean it loads all channels and related stories with a fresh state. However when I used routing this never works as expected. For more explanation I use react-redux-router to make sure all the states are synchronized. So when I use router and and I load another page then when I get back to my channel page everything is repeated twice. so for instance if I have 2 channel with 3 stories in each, then in the result page after routing I have 4 channels with 6 stories in each channel. This means that react does not clear the states and adds everything on top of current state Here is my code:
import React from "react";
import {connect} from "react-redux";
import StoryBoard from '../story-board/StoryBoard';
import {getChannels} from './action/ChannelAction';
import {updateChannels} from './action/ChannelUpdateAction';
import {cleanStoryBoards} from '../story-board/story-board-action/CleanStoryBoardsAction';
class Channel extends React.Component {
constructor(props) {
super();
}
componentDidMount() {
this.props.getChannels();
}
render() {
return (
<div>
<div className="col-xs-12 col-md-8 col-lg-8 paddingStoryBoardsDownJs dummyEachChannelStoryBoard">
<div className="row">
{
this.props.channels.channelsArr.map((item, i) => <StoryBoard
newsChanel={item}
idForDummySetUp={i + 1}
key={"storyBoard" + i}
channelsInfo={{
"channelsCount": this.props.channels.channelsArr.length,
"channelIndex": i
}}></StoryBoard>)
}
</div>
</div>
<div className="col-xs-12 col-md-2 col-lg-2 color2">.col-sm-4</div>
</div>
);
}
}
const mapStateToProps = (state) => {
return {
channels: state.channelReducer
};
};
const mapDispatchToProps = (dispatch) => {
return {
getChannels: () => {
dispatch(getChannels());
}
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Channel);
And also in channel action I have:
export function getChannels(){
return dispatch => {
$.ajax({
url: "http://localhost:3003/jsonchannel.txt",
dataType: 'json',
cache: false,
success: function(data) {
var arr=[];
for (var key in data.channelList) {
arr.push(data.channelList[key].capitalizeFirstLetter());
}
dispatch({
type: "GET_CHANNEL",
payload: arr
});
}.bind(this)
});
};
}
And in channel reducer I have:
const ChannelReducer = (state ={"channelsArr":[],"channelLabelForScrolls":[]}, action) => {
switch (action.type) {
case "GET_CHANNEL":
state={"channelsArr":action.payload};
break;
}
return state;
};
export default ChannelReducer;
Also my storyboard is as follows:
import React from "react";
import {connect} from "react-redux";
import {getStoriesAction} from './story-board-action/StoryBoardAction';
import {HamburgerMenu} from './storyboard-classes/HamburgerMenuShouldBeRemoved';
class StoryBoard extends React.Component {
constructor(props) {
super();
}
componentDidMount() {
/**Loading Stories**/
this.props.getStories(this.props.newsChanel);
}
render() {
return (
<div>
<div id={"dummyStoryBoardHeaderRowJs" + this.props.idForDummySetUp}
className="row storyBoardHeaderRowJs">
<StoryBoardHeader idForDummySetUp={this.props.idForDummySetUp} newsChanel={this.props.newsChanel}/>
</div>
<div className="row" id={"channelPositionFinder" + this.props.newsChanel.removeAllSpaces()}>
{
//this.props.stories.map((item,i)=> <Story key={i} position={i} story={item} ></Story>)
this.props.stories.map(function (snippet) {
if ( snippet.channel.toLowerCase() === this.props.newsChanel.toLowerCase()) {
return (
snippet.storiesSnippet.map((item, i) => <Story key={i} story={item}
channel={this.props.newsChanel}></Story>)
);
}
}.bind(this)) //bind thid to outer loop to mae parent this valid
}
</div>
</div>
);
}
componentDidUpdate() {
}
}
const mapStateToProps = (state) => {
return {
stories: state.storyBoardReducer
};
};
//which actions we wanna use in this components
const mapDispatchToProps = (dispatch) => {
return {
getStories: (chanel) => {
dispatch(getStoriesAction(chanel));
}
};
};
export default connect(mapStateToProps, mapDispatchToProps)(StoryBoard);
and my story action is as follows:
export function getStoriesAction(channel){
return dispatch => {
$.ajax({
url: "http://localhost:3003/"+channel.replace(/\s+/g, '')+".txt",
dataType: 'json',
cache: false,
success: function(data) {
var storiesSnoppet=[];
for (var key in data) {
storiesSnoppet.push(data[key]);
}
console.log("channel: "+channel );
console.log(storiesSnoppet);
dispatch({
type: "SET_STORIES",
payload: {"channel":channel,"storiesSnippet":storiesSnoppet}
});
}.bind(this)
});
};
}
and finally reducer for storyboard is as follows:
const StoryBoardReducer = (state =[], action) => {
switch (action.type) {
case "SET_STORIES":
var tempStateStories = state.slice();
tempStateStories.push(action.payload);
state=tempStateStories;
break;
}
return state;
};
export default StoryBoardReducer;
As far as I know I took everything into account and everything works but when I use routing channels and stories inside each channel gets doubled which means that react router adds channels and stories on top of the old state rather than starting with fresh new state. For sake space I did not added the pages which rerout back to channel since it is simply a link. Can anyone help?
You should use the componentWillUnmount() life cycle method to clear the state before you navigate to another page. Provide a props method to flush the state you are reading from, and use it in the componentWillUnmount() method of this component.