React-Native Redux connected component not dispatching actions - javascript

I have connected my component to redux, defined mapStateToProps and mapDispatchToProps and still dispatch doesn't seem to be doing anything.
When I run this, 'console.log("Setting token")' is printed out, but nothing else happens. So this.props.setToken is firing, because that's where that console.log is set, but the store isn't updating and this.props.auth_token isn't showing on the screen like it should if dispatch(setToken) had fired correctly...
AppScreen
import React from 'react';
import { View, Text, ActivityIndicator } from 'react-native';
import { connect } from 'react-redux';
import { setToken } from './reducer';
class AppScreen extends React.Component {
constructor(props) {
super(props);
}
componentDidMount() {
this.props.setToken("token_abc");
}
render() {
if (this.props.loading) {
return (
<View style={{ flex: 1 }}>
<ActivityIndicator />
</View>
)
} else {
return (
<View>
<Text>You're in! '{this.props.auth_token}'</Text>
</View>
)
}
}
}
function mapStateToProps(state) {
return {
user: state.user,
auth_token: state.auth_token,
loading: state.loading,
error: state.error
};
}
const mapDispatchToProps = dispatch => {
return {
setToken: token => {
console.log("Setting token")
dispatch(setToken(token));
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(AppScreen);
Reducer
import { createSlice } from "#reduxjs/toolkit";
const appSlice = createSlice({
name: "app",
initialState: {
loading: true,
auth_token: "",
error: "",
user: {}
},
reducers: {
setToken: (state, action) => {
state.auth_token = action.payload;
state.loading = false;
},
},
extraReducers: {
}
});
export const { setToken } = appSlice.actions;
export const appReducer = appSlice.reducer;
Store
import { appReducer } from "./App/reducer";
import { configureStore, getDefaultMiddleware } from "#reduxjs/toolkit";
const middleware = [
...getDefaultMiddleware(),
]
const store = configureStore({
reducer: {
app: appReducer
},
middleware,
});
export default store;

You've got a mismatch between the combined store reducer setup, and what your mapState is trying to do.
Your component expects that state.auth_token will exist. The appSlice.reducer has an auth_token field inside. Therefore, state.auth_token will only exist if appSlice.reducer is the root reducer for the entire store.
However, you're currently passing appSlice.reducer as a "slice reducer" for state.app, which will result in state.app.auth_token.
You need to either pass reducer: appReducer to make it be the root reducer by itself, or update your mapState to read state.app.auth_token.
Given that you're likely going to have other slices, I'd recommend the latter approach.

Related

Redux store property gets nested into itself on update

I'm having this issue where my props are ending up looking like this (on console.log):
{
fetchRoles: f(),
roles:
roles: ["Admin", "Manager"],
}
As you can see, somewhere in my code I'm making a mistake that causes the roles prop to get nested into itself, which would force me into doing const { roles } = this.props.roles; in order to retrieve my data (which works BTW).
I've looked around for help but not many people seem to have run into this issue (I'm just getting started with redux).
Below you can see my files:
rolesReducer.js:
import { FETCH_ROLES } from "../actions/types";
const initialState = {
roles: [],
};
export default function (state = initialState, action) {
const { roles } = action;
switch (action.type) {
case FETCH_ROLES:
return {
...state, //also tried ...state.roles and same issue.
roles,
};
default:
return state;
}
}
rolesActions.js:
import { FETCH_ROLES } from "./types";
const roles = ["SuperAdmin"];
export function fetchRoles() {
return function (dispatch) {
dispatch({
type: FETCH_ROLES,
roles,
});
};
}
reducers/index.js (root reducer):
import { combineReducers } from "redux";
import rolesReducer from "./rolesReducer";
import roleMembersReducer from "./roleMembersReducer";
export default combineReducers({
roles: rolesReducer,
roleMembers: roleMembersReducer,
});
PermissionsManager.jsx:
import React, { Component } from "react";
import { connect } from "react-redux";
import { Container } from "react-bootstrap";
import { fetchRoles } from "../redux/actions/rolesActions";
import RoleContainer from "./RoleContainer";
class PermissionsManager extends Component {
componentDidMount() {
this.props.fetchRoles();
}
render() {
console.log(this.props);
const { roles } = this.props.roles;
return (
<Container>
{roles.map((role) => {
return <RoleContainer key={role} role={role} />;
})}
</Container>
);
}
}
const mapStateToProps = (state) => {
return {
roles: state.roles,
};
};
export default connect(mapStateToProps, { fetchRoles })(PermissionsManager);
Edit 1 - Adding reducer log:
As suggested, I logged the reducer, specifically state and action:
state:
{
roles: [],
}
action:
{
roles: ["Admin", "Manager"],
type: "FETCH_ROLES",
}
No duplication or abnormal structures I believe.
One way to shape the store like you're asking is to flatten roles in rolesReducer.js,
you can do so storing the received array directly in the partial state:
initialState would need to look like
const initialState = []
and in the switch statement
case FETCH_ROLES:
return roles

React Native: TypeError: undefined is not an object (evaluating '_this.props.data.map')

I wonder if React Native has a bug that needs fixing that gives the following error:
React Native: TypeError: undefined is not an object (evaluating
'_this.props.data.map')
I am pretty good at this and yet I cannot seem to resolve why I am getting this error when I put together this component:
import React, { Component } from "react";
import { View, Animated } from "react-native";
class Swipe extends Component {
renderCards() {
return this.props.data.map(item => {
return this.props.renderCard(item);
});
}
render() {
return <View>{this.renderCards()}</View>;
}
}
export default Swipe;
I have checked and double checked through various debugging practices that the problem is not with my action creator or reducer and after various refactors I got those working correctly.
I decided to do the above component from scratch whereas before I was reusing another component and yet I still get the above error.
I ask if it's a bug with RN because someone else posted a similar problem but they did not get the answer they needed.
It is not a scope issue with this because if I refactor it like so:
renderCards = () => {
return this.props.data.map(item => {
return this.props.renderCard(item);
});
};
It does absolutely nothing for me, same error message. The message saying is not an object is confusing too, it's an array and map() can only iterate through arrays, so not sure what not being an object has to do with it.
The above component is being called in this screen:
import React, { Component } from "react";
import { View, Text } from "react-native";
import { connect } from "react-redux";
import Swipe from "../components/Swipe";
class DeckScreen extends Component {
renderCard(job) {
return (
<Card title={job.title}>
<View style={styles.detailWrapper}>
<Text>{job.company}</Text>
<Text>{job.post_date}</Text>
</View>
<Text>
{job.description.replace(/<span>/g, "").replace(/<\/span>/g, "")}
</Text>
</Card>
);
}
render() {
return (
<View>
<Swipe data={this.props.jobs} renderCard={this.renderCard} />
</View>
);
}
}
const styles = {
detailWrapper: {
flexDirection: "row",
justifyContent: "space-around",
marginBottom: 10
}
};
function mapStateToProps({ jobs }) {
return { jobs: jobs.listing };
}
export default connect(mapStateToProps)(DeckScreen);
This is what the action creator looks like:
import axios from "axios";
// import { Location } from "expo";
import qs from "qs";
import { FETCH_JOBS, LIKE_JOB } from "./types";
// import locationify from "../tools/locationify";
const JOB_ROOT_URL = "https://authenticjobs.com/api/?";
const JOB_QUERY_PARAMS = {
api_key: "5634cc46389d0d872723b8c46fba672c",
method: "aj.jobs.search",
perpage: "10",
format: "json"
};
const buildJobsUrl = () => {
const query = qs.stringify({ ...JOB_QUERY_PARAMS });
return `${JOB_ROOT_URL}${query}`;
};
export const fetchJobs = (region, callback) => async dispatch => {
try {
const url = buildJobsUrl();
let { data } = await axios.get(url);
dispatch({ type: FETCH_JOBS, payload: data });
callback();
} catch (e) {
console.log(e);
}
};
export const likeJob = job => {
return {
payload: job,
type: LIKE_JOB
};
};
and reducer:
import { FETCH_JOBS } from "../actions/types";
const INITIAL_STATE = {
listing: []
};
export default function(state = INITIAL_STATE, action) {
switch (action.type) {
case FETCH_JOBS:
return action.payload;
default:
return state;
}
}
and the combineReducer is setup correctly as well:
import { combineReducers } from "redux";
import auth from "./auth_reducer";
import jobs from "./jobs_reducer";
import likedJobs from "./likes_reducer";
export default combineReducers({
auth,
jobs,
likedJobs
});
The listing: [] is based off the structure of the response I get back. When I console.log(data);, the actual data I care about is inside of listing property. So I set up the INITIAL_STATE to default listing to be an empty array with the intent to ensure I could map over the array and not worry about the case where I have not yet fetched the list of jobs. When I go to the API endpoint directly you can see it below:
I think the problem is simply that this.props.jobs is undefined. Your initial state is defined as { listing: [] }, however you mapStateToProps do { jobs: ... }.
Try changing initialState to { jobs: [] }, so that it always work on your first rendering.
I think your mapStateToProps should be:
mapStateToProps = (state) => {
return { jobs: listings.listing }
}
EDIT
Actually, it could be even better if you 'name' your state correctly in your reducer, like:
const INITIAL_STATE = { jobs: [] }
export default function(state = INITIAL_STATE, action) {
switch (action.type) {
case FETCH_JOBS:
const jobs = action.payload.listings.listing
return { ...state, jobs };
default:
return state;
}
}
Then in your mapStateToProps:
mapStateToProps = ({ jobs }) => {
return { jobs }
}
The issue is in your reducer. Please refer the below changes:
import { FETCH_JOBS } from "../actions/types";
const INITIAL_STATE = {
listing: []
};
export default function(state = INITIAL_STATE, action) {
switch (action.type) {
case FETCH_JOBS:
const { listings } = action.payload
return {...state, listing: listings.listing}
default:
return state;
}
}
Hope this will help.
function mapStateToProps({ jobs }) {
return { jobs: jobs.listing };
}
the above is making confusion for you try the below one
try to put
function mapStateToProps( state ) {
return { jobs: state.jobs.listing };
}
as you have defined your reducer as follow
export default combineReducers({
auth,
jobs,
likedJobs
});
jobs is your variable to access jobs reducer

Re render Component when props updates (w/ Redux)

Ok so i have 2 components, Map and App. App component basically holds the map.
This is the code of my App:
import React, { Component } from 'react';
import { Text, View } from 'react-native';
import { StackNavigator, TabNavigator } from 'react-navigation';
//Redux
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers';
//Screen Imports
import CenterNew from './screens/CenterNew';
import Map from './components/Map';
import Profile from './containers/Profile';
import Review from './containers/Review';
import { connect } from 'react-redux';
var store = createStore(rootReducer, {}, applyMiddleware(thunk));
class App extends Component {
render () {
{ console.ignoredYellowBox = ['Remote debugger']; }
const MainScreenNavigator = TabNavigator({
Centers: {screen: Map},
Review: {screen: Review},
Profile: {screen: Profile}
});
MainScreenNavigator.navigationOptions = {
title: this.props.map.title
};
const Screens = StackNavigator({
Home: {screen: MainScreenNavigator},
CenterNew: {screen: CenterNew}
});
return (
<Provider store={store}>
<Screens />
</Provider>
);
}
}
const connectWithStore = (store, WrappedComponent, mapStateToProps) => {
let ConnectedWrappedComponent = connect(mapStateToProps)(WrappedComponent);
return (props) => {
return <ConnectedWrappedComponent {...props} store={store} />
}
}
const mapStateToProps = (state) => {
return {
map: state.map
};
};
App = connectWithStore(createStore(rootReducer, {}, applyMiddleware(thunk)), App, mapStateToProps);
export default App;
In the snippet, i set the title using
MainScreenNavigator.navigationOptions = {
title: this.props.map.title
};
So whenever App is rendered, I log the initial state as you could see in componentWillMount() and this is the result. A syou can see, the initial title is 'Home'
now, in my Map I have a button which triggers this action creator just to change the header text:
this.props.changeHeaderTitle('Test');
console.log(this.props.map);
code:
export const changeHeaderTitle = (title) => {
return {
type: 'CHANGE_HEADER_TITLE',
payload: title
};
};
and this is my reducer:
const INITIAL_STATE = {
isPinnable: false,
pinnedCoordinateLatitude: 0,
pinnedCoordinateLongitude: 0,
region: {
latitude: 14.582524,
longitude: 121.061547,
latitudeDelta: 0.007,
longitudeDelta: 0.007
},
title: '',
searched: false
};
export default (state = INITIAL_STATE, action) => {
switch(action.type) {
case 'ENABLE_PINNING':
return { ... state, isPinnable: true }
case 'DISABLE_PINNING':
return { ... state, isPinnable: false,
pinnedCoordinateLatitude: action.payload.latitude,
pinnedCoordinateLongitude: action.payload.longitude }
case 'GET_PINNED_COORDINATE':
let test = action.payload.longitude;
return {
...state,
isPinnable: false,
pinnedCoordinateLatitude: action.payload.latitude,
pinnedCoordinateLongitude: action.payload.longitude
}
case 'GET_CURRENT_REGION':
return { ...state, region: region }
case 'CHANGE_CURRENT_REGION':
return { ...state, region: action.payload}
case 'CHANGE_HEADER_TITLE':
return { ...state, title: action.payload }
case 'SEARCHED_VIEW':
return { ...state, searched: action.payload }
default:
return state;
}
};
whenever the action is triggered, i know the title is updated because i log it as you can see in
this.props.changeHeaderTitle('Test');
console.log(this.props.map);
but in my view, the title is still 'Home'.. I referred to this: rerender react component when prop changes and tried to use componentWillRecieveProps() but it is not logging when this.props.changeHeaderTitle('Test'); action is triggered from Map component.
You need to connect your Redux store with your component. Therefore you should add this snippet:
mapStateToProps(store){
const { title } = store;
return { title };
}
export default connect(mapStateToProps, { changeHeaderTitle })(App)
The mapStateToProps function will subscribe to redux store updates. Every time your state changes, your component will rerender automatically. You can find more information about connecting components to the redux store here: react-redux api docs
You can't update the screen title like in the code you provide.
//This is only needed if you want to set the title from another screen
static navigationOptions = ({ navigation }) => {
const {state} = navigation;
return { title: `${state.params.title}`,
};
};
ChangeThisTitle = (titleText) => {
const {setParams} = this.props.navigation; setParams({ title: titleText })
}
//Your render method
render(){
//Views goes here
}
To change the title you have call the change title method from the onPress.
You may please refer this link for more details.

React-Redux: How do I populate a component's prop on load with data from an async call?

I have this autocomplete component that takes an array of terms as a dataSource prop. The data I want to feed in resides in a public API, and I've followed the tutorial here to get to the code below. But this tutorial (and many others out there) explain how to bind these actions to an event, whereas I want to populate this prop with data on page load. How would I go about doing that?
actions.js
import fetch from 'isomorphic-fetch';
export function loadSchools(termId) {
return {
type: 'LOAD_SCHOOLS',
termId
};
}
export function receiveSchools(termId, json) {
return {
type: 'RECEIVE_SCHOOLS',
termId,
schools: json.data.children.map(child => child.data), // ???
receivedAt: Date.now()
};
}
export function getSchools(termId) {
return function (dispatch) {
dispatch(loadSchools(termId));
return fetch('http://www.northwestern.edu/class-descriptions/4650/index-v2.json')
.then(response => {
if (response.status >= 400) {
throw new Error('Bad response from server');
}
return response.json();
})
.then(data => dispatch(receiveSchools(termId, data)));
};
}
reducers.js
const initialState = {
schoolsData: {
isFetching: false,
lastUpdated: 0,
schools: []
}
};
function schools(state = initialState, action) {
switch (action.type) {
case 'LOAD_SCHOOLS':
return {
...state,
isFetching: true
};
case 'RECEIVE_SCHOOLS':
return {
...state,
isFetching: false,
schools: action.schools,
lastUpdated: receivedAt
}
default:
return state;
}
}
export default schools;
Search.jsx
import React from 'react';
import AutoComplete from 'material-ui/AutoComplete';
export default class Search extends React.Component {
render() {
return (
<AutoComplete
hintText="Search for something."
dataSource={this.props.searchdata}
maxSearchResults={15}
filter={AutoComplete.caseInsensitiveFilter}
onNewRequest={}
/>
);
}
}
Search.propTypes = {
searchdata: React.PropTypes.array.isRequired,
onSelect: React.PropTypes.func
};
index.jsx
import 'babel-polyfill';
import React from 'react';
import ReactDOM from 'react-dom';
import getMuiTheme from 'material-ui/styles/getMuiTheme';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import { grey500, white, fullBlack } from 'material-ui/styles/colors';
import { fade } from 'material-ui/utils/colorManipulator';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import thunkMiddleware from 'redux-thunk';
import schools from './reducers/reducers';
import colors from './colors';
import NavBar from './components/NavBar.jsx';
import Serif from './components/Serif.jsx';
const store = createStore(schools, applyMiddleware(thunkMiddleware));
const muiTheme = getMuiTheme({
palette: {
primary1Color: colors.northwesternPurple,
primary2Color: colors.northwesternPurple120,
primary3Color: grey500,
accent1Color: colors.northwesternPurple30,
accent2Color: colors.richBlack10,
accent3Color: colors.richBlack50,
textColor: colors.richBlack80,
alternateTextColor: white,
canvasColor: white,
borderColor: colors.richBlack20,
disabledColor: fade(colors.richBlack80, 0.3),
pickerHeaderColor: colors.northwesternPurple,
clockCircleColor: fade(colors.richBlack80, 0.07),
shadowColor: fullBlack
}
});
class App extends React.Component {
render() {
return (
<Provider store={store}>
<MuiThemeProvider muiTheme={muiTheme}>
<div> {/* MuiThemeProvider requires stricly one child element */}
<NavBar />
<Serif /> {/* This component contains SearchContainer, which in turn contains Search */}
</div>
</MuiThemeProvider>
</Provider>
);
}
}
ReactDOM.render(<App />, document.getElementById('app'));
You can render your Search component from another component, let's call it SearchContainer. SearchContainer is decorated by the connect function from react-redux that has as only role to dispatch the action to fetch the schools. SearchContainer doesn't render Search component until the school are fetched.
Here an example of what the code would look like. Here I assume you don't use react-redux.
First you have a small problem in your initial state in reducers.js. It should be:
const initialState = {
isFetching: false,
lastUpdated: 0,
schools: []
};
function schools(state = initialState, action) {
switch (action.type) {
case 'LOAD_SCHOOLS':
return {
...state,
isFetching: true
};
case 'RECEIVE_SCHOOLS':
return {
...state,
isFetching: false,
schools: action.schools,
lastUpdated: receivedAt
}
default:
return state;
}
}
SearchContainer.js
// ./containers/SearchContainer.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { loadSchools } from '../actions/actions'
import Search from '../components/Search';
class SearchContainer extends Component {
componentDidMount() {
this.props.loadSchools(this.props.termId);
},
render() {
const {
schools,
isFetching
} = this.props;
if (isFetching) {
return null;
}
return <Search schools={schools} />;
}
}
const mapStateToProps = (state) => ({
isFetching: state.isFetching,
schools: state.schools
});
const mapActionsToProps = (dispatch) => ({
loadSchools: (termId) => dispatch(loadSchools(termId)),
});
export default connect(mapStateToProps, mapActionsToProps)(SearchContainer);
In this way, at the first render, your Search component is not rendered. It is rendered, only after the schools are loaded.
You can dispatch the LOAD_SCHOOLS action from the componentDidMount lifecycle method (maybe in your Serif component but I can't see the code for that).
From the docs:
componentDidMount() is invoked immediately after a component is mounted. Initialization that requires DOM nodes should go here. If you need to load data from a remote endpoint, this is a good place to instantiate the network request. Setting state in this method will trigger a re-rendering.
https://facebook.github.io/react/docs/react-component.html#componentdidmount

React redux display data from async action

I am building a react app and implementing redux for data. When I navigate to a particular route, I want to dispatch the action to fetch the data from an external API and then once the data comes back, display the data for the user.
Store :
import { createStore, applyMiddleware } from 'redux';
import thunkMiddleware from 'redux-thunk';
import createLogger from 'redux-logger';
import { syncHistoryWithStore } from 'react-router-redux';
import { browserHistory } from 'react-router';
import rootReducer from '../reducers/index';
const initialState = {
marvel :{
characters: []
}
};
const store = createStore(rootReducer, initialState, applyMiddleware(thunkMiddleware, createLogger()));
export const history = syncHistoryWithStore(browserHistory, store);
if (module.hot) {
module.hot.accept('../reducers/', () => {
const nextRootReducer = require('../reducers/index').default;
store.replaceReducer(nextRootReducer);
});
}
export default store;
reducer :
import * as constants from '../constants/constants';
const initialState = {
characters: [],
isFetching: false,
errorMessage: null
};
const marvelReducer = (state = initialState, action) => {
switch (action.type) {
case constants.FETCH_MARVEL :
return Object.assign({},state,{isFetching: true});
case constants.FETCH_MARVEL_SUCCESS :
return Object.assign({}. state,{
characters: [...action.response],
isFetching: false
});
case constants.FETCH_MARVEL_ERROR :
return Object.assign({}, state,{
isFetching: false,
errorMessage: action.message
});
default :
return state;
}
};
export default marvelReducer;
actions:
import 'whatwg-fetch';
import * as constants from '../constants/constants';
export const fetchMarvel = (dispatch) => {
const MARVEL_API = 'http://gateway.marvel.com:80/v1/public/characters?apikey=e542b1d89f93ed41b132eda89b9efb2c';
dispatch({
type: constants.FETCH_MARVEL
});
return fetch(MARVEL_API).then(
response => {
dispatch({
type: constants.FETCH_MARVEL_SUCCESS,
response
});
},
error => {
dispatch({
type: constants.FETCH_MARVEL_ERROR,
message: error.message || 'Something went wrong with fetchMarvel'
});
});
};
component :
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as actions from '../actions/actions';
import '../styles/homeStyles.css';
class Home extends Component {
render() {
const { characters, isFetching, errorMessage } = this.props;
return (
<div>
<h1>React Starter App</h1>
<h2>This is the home page</h2>
</div>
);
}
}
function mapStateToProps(state) {
return {
characters: state.characters,
isFetching: state.isFetching,
errorMessage: state.errorMessage
};
}
function mapDispatchToProps(dispatch) {
return { actions: bindActionCreators(actions, dispatch) };
}
export default connect(mapStateToProps, mapDispatchToProps)(Home);
I know I'm not currently displaying the props anywhere in the application, but first I am just trying to get them populated.
What step am I missing to dispatch the action so that I can populate the props from state?
You are not dispatching the action anywhere. So nothing happens.
You probably want to do this in a React lifecycle hook, for example:
class Home extends Component {
componentDidMount() {
this.props.actions.fetchMarvel();
}
render() {
const { characters, isFetching, errorMessage } = this.props;
return (
<div>
<h1>React Starter App</h1>
<h2>This is the home page</h2>
</div>
);
}
}

Categories

Resources