I'm trying to map this array but the it keeps saying .includes is undefined. I assume maybe its because I've mapped is wrong? I'm not sure but I will list everything I have.
Here is what my api data looks like when I console.log(this.props.tournaments it from from my redux store:
console.log
You see the undefined in the first index of the array, I've used an Object.keys(tournaments).map(key => tournaments[key]) and .map (which you can see in the component snippet below) twice which when I console.log I get the correct results but I still get an error when its passes through the .filter function.
Here is what it looks like after I've formatted the array:
console.log after formatting
but I'm still getting an error...
This the error I'm getting:
Error message
Here is the component in question:
import React from 'react';
import { connect } from 'react-redux';
import { fetchTournaments } from '../actions/tournaments';
class Search extends React.PureComponent {
// State only needs to hold the current filter text value:
componentDidMount() {
this.props.fetchTournaments();
}
state = {
filterText: ''
};
handleChange = event => {
this.setState({ filterText: event.target.value });
};
render() {
let tourns = this.props.tournaments.map(tore =>
tore.map(room => room.name)
);
console.log(tourns, 'here');
const filteredList = tourns.filter(item =>
item.name.includes(this.state.filterText)
);
return (
<React.Fragment>
<input onChange={this.handleChange} value={this.state.filterText} />
<ul>{filteredList.map(item => <li key={item.id}>{item.name}</li>)}</ul>
</React.Fragment>
);
}
}
function mapStateToProps({ tournaments }) {
return {
tournaments: Object.keys(tournaments).map(key => tournaments[key])
};
}
export default connect(mapStateToProps, {
fetchTournaments
})(Search);
My data is coming from redux like so:
Reducer:
import _ from 'lodash';
import {
FETCH_TOURNAMENT,
from '../actions/types';
export default (state = {}, action) => {
switch (action.type) {
case FETCH_TOURNAMENT:
return { ...state, [action.payload.id]: action.payload };
default:
return state;
}
};
action:
import {
FETCH_TOURNAMENT,
} from './types';
import { API_TOURNAMENTS_URL } from '../constants/api';
import axios from 'axios';
export const fetchTournament = id => async dispatch => {
const response = await axios.get(`http://localhost:4000/tournaments/${id}`);
dispatch({ type: FETCH_TOURNAMENT, payload: response.data });
};
I think you could treat you response.data before setting to state to reflect more how you will be handling your application. I would imagine that you would rather have an array of objects.
you could try passing to payload Object.values(response.data).flat() instead which would give you an array of of objects based on your response.
Edit
below is without implementing Object.values(response.data).flat(). if you follow the suggestion you shouldnt need to treat
about the issue at filter, item is an array containing name value at index 0. this is why you get undefined. this happens because of tore.map(room => room.name) returns an array.
if you change as below you may get your array of tourns:
let tourns = this.props.tournaments.flat();
but I would consider to treat your response which would avoid all these changes on component level. pick one which suits you better.
Related
From this guide I found here:
https://daveceddia.com/where-fetch-data-redux/
I have a pretty standar reducer that handle data, loading and error:
import {
FETCH_PRODUCTS_BEGIN,
FETCH_PRODUCTS_SUCCESS,
FETCH_PRODUCTS_FAILURE
} from './productActions';
const initialState = {
items: [],
loading: false,
error: null
};
export default function productReducer(state = initialState, action) {
switch(action.type) {
case FETCH_PRODUCTS_BEGIN:
return {
...state,
loading: true,
error: null
};
case FETCH_PRODUCTS_SUCCESS:
return {
...state,
loading: false,
items: action.payload.products
};
case FETCH_PRODUCTS_FAILURE:
return {
...state,
loading: false,
error: action.payload.error,
items: []
};
default:
return state;
}
}
And then the component that call the action and draw based on those states:
import React from "react";
import { connect } from "react-redux";
import { fetchProducts } from "/productActions";
class ProductList extends React.Component {
componentDidMount() {
this.props.dispatch(fetchProducts());
}
render() {
const { error, loading, products } = this.props;
if (error) {
return <div>Error! {error.message}</div>;
}
if (loading) {
return <div>Loading...</div>;
}
return (
<ul>
{products.map(product =>
<li key={product.id}>{product.name}</li>
)}
</ul>
);
}
}
const mapStateToProps = state => ({
products: state.products.items,
loading: state.products.loading,
error: state.products.error
});
export default connect(mapStateToProps)(ProductList);
This works fine the very first time:
products is empty. So the first render will show the empty list. The second time(after the fetch is completed) products will have items.
Now, the problem is what happens if I get outside then component and then re-enter(for example, using react-router).
The very first time It will draw with the cached information in the redux-store. Then after the fetch I will redraw the new list.
Is there any way to avoid this every time?
i have thought a couple of "solutions" but I'm not soure if they will work/are good practices:
setting in the component state a value "fetchId" (example, generating a random UUID) and use it in the fetchProducts action. That value would be saved in the redux store and the compare the redux fetchId with the component. If they are the same, DRAW! If they are differente(the fetchId comes from a different call) I will not draw anything.
Cleaning up redux store calling an action in the componentWillUmount
Store the product I'd in Redux and only render the product if it matches the one the user has selected. If it doesn't match, fetch the requested product.
I'm working in an existing codebase that uses the React, Meteor, and react-meteor-data combo.
Everything has been going relatively fine up until I tried implementing a search feature using withTracker,
React Select,
and Meteor's subscription functionality.
import { CollectionAPI } from '../arbitrary_meteormongo_collection';
export const WriteableConnectionToCollection = withTracker(props => {
let connection = Meteor.subscribe('COLLECTION_NAME.searchByName', SEARCH_TEXT_HERE);
let isLoading = connection.ready();
return {
...props,
isLoading: isLoading,
collection: CollectionAPI.find().fetch()
}
})(PRESENTATIONAL_COMPONENT);
I've googled around and saw that a common solution for getting data to Meteor.subscribe is to use things like URL parameters, though as I am working in an existing codebase, this change would also need to be implemented in various locations.
Another way I have found is to pass the input field's value to the parent component by keeping track of the input field state in the parent component's state, though this is clearly breaking the principal of separation of concerns:
Parent Component
export const ParentComponent = React.createClass({
getInitialState() {
return {
inputFieldValue: undefined
}
},
onChange(change) {
this.setState(inputFieldValue);
},
render() {
return (
<Search
onChange={this.onChange}
inputFieldValue={this.state.inputFieldValue}
/>
}
}
withTracker HOC
import { CollectionAPI } from '../arbitrary_meteormongo_collection';
export const WriteableConnectionToCollection = withTracker(props => {
let connection = Meteor.subscribe('COLLECTION_NAME.searchByName', this.props.inputFieldValue);
let isLoading = connection.ready();
return {
...props,
isLoading: isLoading,
collection: CollectionAPI.find().fetch()
}
});
InputField Component
import { WriteableConnectionToCollection } from './connections/writeableconnection.js';
const InputFieldComponent = React.createClass({
render() {
<InputField
onInputChange={this.props.onChange}
/>
}
}
export default WritableConnectionToCollection(InputFieldComponent);
Is this the only way to do things with this particular package/framework combo or is there a simpler way that I'm just not seeing?
As Christian Fritz had mentioned in a comment under my original question, I can use ReactiveVar to be able to pass input in and out of my connection component:
export const WritableConnection = function (subscriptionName, collectionAPI) {
/**
* ReactiveVar must be outside of withTracker. If the it was inside withTracker's scope,
* anytime a user would use .set(ANY_VALUE), it would overwrite whatever was in it first,
* and then re-initialize.
**/
const input = new ReactiveVar(undefined);
return withTracker(props => {
const connection = Meteor.subscribe(subscriptionName, input.get());
const isLoading = connection.ready();
return {
...props,
isLoading: isLoading,
collection: collectionAPI.find().fetch(),
setSearchText: (text) => input.set(text),
getSearchText: () => input.get()
}
})
}
I have a reducer named "leagues" in the redux state tree, which is just an array of individual league objects. Each league has a unique id (assigned in the backend), and a name.
I'm trying to write a Component that represents an individual league, and so I want to have a mapStateToProps function that retrieves the correct league. The correct league is known from the url, that is, through the match prop in react-router-v4.
I tried:
function mapStateToProps(state, ownProps) {
return {
league: state.leagues.find(aLeague => aLeague.id === ownProps.match.params.id)
}
}
But that led to an error "state.leagues.find" is not a function.
Then I tried
function mapStateToProps(state, ownProps) {
return {
league: state.leagues[ownProps.match.params.id]
}
}
which doesn't error, but retrieves the wrong league - if the id is 3, then this retrieves state.leagues[3], instead of state.leagues.where(league.id === 3)
The value of leagues when I attempt to access the a single league's page:
leagues: [
{
id: 54,
name: 'Test League'
},
{
id: 55,
name: 'Another Test'
}
],
And the leagues reducer code:
const initialState = {
leagues: []
};
export default (state = initialState, action = {}) => {
switch(action.type) {
case SET_USER_LEAGUES:
return state = action.leagues
case ADD_USER_LEAGUE:
return [
...state,
{
id: action.league.id,
name: action.league.name,
}
];
default: return state;
}
}
Any help is much appreciated.
Thanks!
This is because when the redux store is initialized you most likely are setting the initial state to null which is why you get an error state.leagues.find is not a function and once the state is resolved through an async ajax call then the state is there. I recommend to make that kind of logic in a react lifecycle method like componentDidMount and set the state there for the league once the leagues state is available. like this:
componentDidMount() {
const { leagues } = this.state;
if (leagues && leagues.length > 0) {
// update the state to the correct league here and this.props.id is the id that you want
const league = leagues.find(aLeague => aLeague.id === this.props.id);
this.setState({
league
}
}
}
I hope that helps.
It seems like when your component first renders, its default state has been set to null and causes the app to crash when you try to use array method find on a null object. Set your initial state for your leagues reducer to an empty array.
Then if your array is empty, your app probably hasn't retrieved results yet, and you can display a message like "Loading...".
However, this doesn't solve the problem of you actually have 0 items in your database, for example. Then you'll show falsely show loading even when there is 0 records.
For that, I would also suggest adding a isLoading reducer (with default state true), that maintains the state of your application during the time it is fetching async data. When your async calls complete, dispatch an action to update the appstate and set isLoading to false. Only then should you try to retrieve values from your leagues reducer.
I would also suggest you have another "league" reducer that does the filtering so you don't have to maintain this logic in your component.
I see that Array.prototype.find is not supported in IE. So, there could be a browser compatibility issue.
You can always use Array.prototype.filter instead (assuming that state.leagues will always be an Array:
function mapStateToProps(state, ownProps) {
const { leagues = [] } = state;
return {
league: leagues.filter(aLeague => aLeague.id === ownProps.match.params.id)[0]
}
}
Your reducer looks wrong to me. Could you try this:
export default (state = initialState, action = {}) => {
switch (action.type) {
case SET_USER_LEAGUES:
return {
...state,
leagues: action.leagues,
};
case ADD_USER_LEAGUE:
const leagues = [...state.leagues, { id: action.league.id, name: action.league.name, }];
return {
...state,
leagues,
};
default:
return state;
}
}
Some of your functions are manipulating the state and changing between returning a bool, an object and an array.
To resolve this, I now use an object instead of an array. (See reference 1 below for the reason). And I render the component after the store state is loaded so I can use mapStateToProps. I've put some code extracts below from working code. Hopefully also provides some tips on how to use Redux reducers for this use case. Please excuse any typos, I edited the code extracts inline here.
Store
import { createStore, applyMiddleware, combineReducers } from 'redux'
import thunkMiddleware from 'redux-thunk'
import * as reducers from './reducers'
const initialState = {
items: {
/* id(1): {id: PropTypes.number.isRequired, name: PropTypes.string}
id(n): {id: PropTypes.number.isRequired, name: PropTypes.string} */
}
}
var rootReducer = combineReducers(reducers)
window.store = createStore(rootReducer, initialState, applyMiddleware(thunkMiddleware))
Action
export const UPDATE_ITEM = 'UPDATE_ITEM'
export const updateItem = item => ({
type: UPDATE_ITEM,
item
})
Reducer
import { UPDATE_ITEM } from './actions'
export const items = (state = null, action) => {
switch (action.type) {
case UPDATE_ITEM:
let id = action.item.id
return Object.assign({}, state, {
[id]: Object.assign({}, state[id], action.item)
})
default:
return state
}
}
Add some objects to the store
import { updateItem } from './actions'
var item1 = {id: 1, name: 'Alice'}
window.store.dispatch(updateItem(item1))
var item2 = {id: 2, name: 'Bob'}
window.store.dispatch(updateItem(item2))
SomeComponent mapStateToProps
function mapStateToProps (state, ownProps) {
return {
item: state.items[ownProps.id]
}
}
Load Component like this after the store is populated.
ReactDOM.render(
<Provider store={window.store}>
<SomeComponent id={1} />
<SomeComponent id={2} />
</Provider>,
document.getElementById('root')
)
Just wanted to note that my main motivation to solve this was that I was mapping the entire object state (state.items) to props, but then render was called after an update to any array element which was horrible for performance.
References:
[1] https://stackoverflow.com/a/36031765/1550587
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.
I am creating a React application and integrating Redux to it in order to manage the state and do network requests.
I followed the Todo tutorial and I am following the async example from the redux website, but I am stucked.
Here is my problem, I want, in my application, to fetch a user from a remote server. So the server send me a json array containing an object (maybe it's better if the server send me directly an object ? )
The json I obtain looks like that (I put only two fields but there are more in real) :
[{first_name: "Rick", "last_name": "Grimes"}]
Anyway I can fetch the data from the server but I can't inject user's data into my application, I hope you can help me but the most important is that I understand why it doesn't work.
Here are my several files :
I have two actions, one for the request and the other for the response:
actions/index.js
export const REQUEST_CONNECTED_USER = 'REQUEST_CONNECTED_USER';
export const RECEIVE_CONNECTED_USER = 'RECEIVE_CONNECTED_USER';
function requestConnectedUser(){
return {
type: REQUEST_CONNECTED_USER
}
}
function receiveConnectedUser(user){
return {
type: RECEIVE_CONNECTED_USER,
user:user,
receivedAt: Date.now()
}
}
export function fetchConnectedUser(){
return function(dispatch) {
dispatch(requestConnectedUser());
return fetch(`http://local.particeep.com:9000/fake-user`)
.then(response =>
response.json()
)
.then(json =>
dispatch(receiveConnectedUser(json))
)
}
}
reducer/index.js
import { REQUEST_CONNECTED_USER, RECEIVE_CONNECTED_USER } from '../actions
function connectedUser(state= {
}, action){
switch (action.type){
case REQUEST_CONNECTED_USER:
return Object.assign({}, state, {
isFetching: true
});
case RECEIVE_CONNECTED_USER:
return Object.assign({}, state, {
user: action.user,
isFetching: false
});
default:
return state;
}
}
And I have finally my container element, that I have called Profile.js
import React from 'react';
import { fetchConnectedUser } from '../actions';
import { connect } from 'react-redux';
class Profile extends React.Component{
constructor(props){
super(props);
}
componentDidMount() {
const { dispatch } = this.props;
dispatch(fetchConnectedUser());
}
render(){
const { user, isFetching} = this.props;
console.log("Props log :", this.props);
return (
<DashboardContent>
{isFetching &&
<div>
Is fetching
</div>
}
{!isFetching &&
<div>
Call component here and pass user data as props
</div>
}
</DashboardContent>
)
}
}
function mapStateToProps(state) {
const {isFetching, user: connectedUser } = connectedUser || { isFetching: true, user: []}
return {
isFetching,
user: state.connectedUser
}
}
export default connect(mapStateToProps)(Profile)
In the code above, I always have the Is Fetching paragraph being display, but even when I remove it, I cannot access to user data.
I think I have the beginning of something because when I console.log I can see my actions being dispatched and the data being added to the state, but I think I am missing something in the link communication with this data to the UI Component.
Am I on the good way or do I have a lot of things to refactor ? Any help would be very helpful !
Seeing as you are immediately fetching the data I allow myself to assume that isFetching may be true at beginning. Add an initial state to your reducer
state = { isFetching: true, user: null }
Then assuming you setup the reducer correctly:
function mapStateToProps(state) {
const {isFetching, user } = state.connectedUser
return {
isFetching,
user
}
}
Hope this works, feels clean-ish.