I created component and connected it using connect() like this:
function mapStateToProps(state) {
return { users: state.users.users }
}
function mapDispatchToProps(dispatch) {
return { userActions: bindActionCreators(UserActions, dispatch) }
}
export default connect(mapStateToProps, mapDispatchToProps)(Test)
Sadly when I open React tools in chrome I get this:
Changing the state isn't forcing component to update. Why it isn't subscribing to it?
EDIT:
I'm changing state through action creator and reducer. This is how it looks like:
export function addUser(user) {
return function (dispatch) {
axios.post('http://localhost:4200/users/add/user', {user:user})
.then(() => dispatch({
type: USER_ADD,
user: user
})).catch(err => console.log(err));
}
}
export function getUsers() {
return function (dispatch) {
axios.get('http://localhost:4200/users')
.then((response) => dispatch({
type: REQUEST_USERS,
users:response.data
})).catch(err => console.log(err));
}
}
and reducer:
export function users(state = initialState, action) {
switch (action.type) {
case 'USER_ADD':
{
return {
...state,
users: [
...state.users,
action.user
]
}
break;
}
case 'REQUEST_USERS':
{
return {
...state,
users: [
action.users
]
}
break;
}
.........
and this is my full component:
class Test extends Component {
constructor(props) {
super(props);
this.state = {
login: "",
password: ""
}
}
handleLoginInputChange = (e) => {
this.setState({login: e.target.value})
}
handlePasswordInputChange = (e) => {
this.setState({password: e.target.value})
}
handleSubmit = (e) => {
e.preventDefault()
let user = {login:this.state.login,
password:this.state.password,userId:Math.floor(Math.random()*(100000))};
this.props.userActions.addUser(user);
}
render() {
return (
<div className="test">
<form>
<input type="text" onChange={this.handleLoginInputChange} value=
{this.state.login} placeholder="login"/>
<input type="text" onChange={this.handlePasswordInputChange}
value={this.state.password} placeholder="pass"/>
<button onClick={this.handleSubmit}>send</button>
</form>
<UsersList users = {this.props.users} />
</div>
)
}
componentDidMount() {
this.props.userActions.getUsers();
}
}
function mapStateToProps(state) {
return { users: state.users.users }
}
function mapDispatchToProps(dispatch) {
return { userActions: bindActionCreators(UserActions, dispatch) }
}
export default connect(mapStateToProps, mapDispatchToProps)(Test)
I've added everything to github so you could see it github.
Things that could interest you are in reducers/users.js, actions/useractions and in components/Test.js.
My state after getting data from server looks like this:.
I tried many different approaches. Right now I gave up changing the state after adding new user. Instead of that I've made button which can get data from the server - it's reloading the page so I'm not pleased with that solution
Are you sure that this is correct?
function mapStateToProps(state) {
return { users: state.users.users }
}
how are you binding your reducer to the store? The state might be different from what you expect in connect. Can you post what the store contains after the component gets mounted?
Related
I have a form in react where I'm asking for the last 8 of the VIN of a car. Once I get that info, I want to use it to get all the locations of the car. How do I do this? I want to call the action and then display the results.
Added reducer and actions...
Here is what I have so far...
class TaglocaByVIN extends Component {
constructor(props){
super(props);
this.state={
searchvin: ''
}
this.handleFormSubmit=this.handleFormSubmit.bind(this);
this.changeText=this.changeText.bind(this);
}
handleFormSubmit(e){
e.preventDefault();
let searchvin=this.state.searchvin;
//I want to maybe call the action and then display results
}
changeText(e){
this.setState({
searchvin: e.target.value
})
}
render(){
return (
<div>
<form onSubmit={this.handleFormSubmit}>
<label>Please provide the last 8 characters of VIN: </label>
<input type="text" name="searchvin" value={this.state.searchvin}
onChange={this.changeText}/>
<button type="submit">Submit</button>
</form>
</div>
);
}
}
export default TaglocaByVIN;
Here are my actions:
export function taglocationsHaveError(bool) {
return {
type: 'TAGLOCATIONS_HAVE_ERROR',
hasError: bool
};
}
export function taglocationsAreLoading(bool) {
return {
type: 'TAGLOCATIONS_ARE_LOADING',
isLoading: bool
};
}
export function taglocationsFetchDataSuccess(items) {
return {
type: 'TAGLOCATIONS_FETCH_DATA_SUCCESS',
items
};
}
export function tagformsubmit(data){
return(dispatch) =>{
axios.get(`http://***`+data)
.then((response) => {
dispatch(taglocationsFetchDataSuccess);
})
};
}
reducer:
export function tagformsubmit(state=[], action){
switch (action.type){
case 'GET_TAG_FORM_TYPE':
return action.taglocations;
default:
return state;
}
}
This is an easy fix but it will take a few steps:
Set up an action
Set up your reducer
Fetch and Render data in component
Creating the Action
The first thing, you need to set up an action for getting data based on a VIN. It looks like you have that with your tagformsubmit function. I would make a few adjustments here.
You should include a catch so you know if something went wrong, change your function param to include dispatch, add a type and a payload to your dispatch, and fix the string literal in your api address. Seems like a lot but its a quick fix.
Update your current code from this:
export function tagformsubmit(data){
return(dispatch) =>{
axios.get(`http://***`+data)
.then((response) => {
dispatch(taglocationsFetchDataSuccess);
})
};
}
to this here:
//Get Tag Form Submit
export const getTagFormSubmit = vin => dispatch => {
dispatch(loadingFunctionPossibly()); //optional
axios
.get(`/api/path/for/route/${vin}`) //notice the ${} here, that is how you use variable here
.then(res =>
dispatch({
type: GET_TAG_FORM_TYPE,
payload: res.data
})
)
.catch(err =>
dispatch({
type: GET_TAG_FORM_TYPE,
payload: null
})
);
};
Creating the Reducer
Not sure if you have already created your reducer. If you have you can ignore this. Creating your reducer is also pretty simple. First you want to define your initial state then export your function.
Example
const initialState = {
tags: [],
tag: {},
loading: false
};
export default (state=initialState, action) => {
if(action.type === GET_TAG_FORM_TYPE){
return {
...state,
tags: action.payload,
loading: false //optional
}
}
if(action.type === GET_TAG_TYPE){
return {
...state,
tag: action.payload,
}
}
}
Now that you have your action and reducer let's set up your component.
Component
I'm going to assume you know all of the necessary imports. At the bottom of your component, you want to define your proptypes.
TaglocaByVIN.propTypes = {
getTagFormSubmit: PropTypes.func.isRequired,
tag: PropTypes.object.isRequired
};
mapStateToProps:
const mapStateToProps = state => ({
tag: state.tag
});
connect to component:
export default connect(mapStateToProps, { getTagFormSubmit })(TaglocaByVIN);
Update your submit to both pass data to your function and get the data that is returned.
handleFormSubmit = (e) => {
e.preventDefault();
const { searchvin } = this.state;
this.props.getTagFormSubmit(searchvin);
const { tags } = this.props;
tags.map(tag => {
//do something with that tag
}
Putting that all together your component should look like this (not including imports):
class TaglocaByVIN extends Component {
state = {
searchvin: ""
};
handleFormSubmit = e => {
e.preventDefault();
const { searchvin } = this.state;
this.props.getTagFormSubmit(searchvin);
const { tags } = this.props.tag;
if(tags === null){
//do nothing
} else{
tags.map(tag => {
//do something with that tag
});
};
}
changeText = e => {
this.setState({
searchvin: e.target.value
});
};
render() {
return (
<div>
<form onSubmit={this.handleFormSubmit}>
<label>Please provide the last 8 characters of VIN: </label>
<input
type="text"
name="searchvin"
value={this.state.searchvin}
onChange={this.changeText}
/>
<button type="submit">Submit</button>
</form>
</div>
);
}
}
TaglocaByVIN.propTypes = {
getTagFormSubmit: PropTypes.func.isRequired,
tag: PropTypes.object.isRequired
};
const mapStateToProps = state => ({
tag: state.tag
});
export default connect(
mapStateToProps,
{ getTagFormSubmit }
)(TaglocaByVIN);
That should be it. Hope this helps.
I have a question about passing value (item.id) from one component to another component's saga, where I could add additional field in POST body and make a request.
I have two components: 1st Form component, where is two input fields. 2st component is Item, which are GET'ed from API. So there is a itemId value, which I need to give when making POST request with form.
My soliution right now is to pass itemId to localstorage and then take it in saga, but it causes some bugs when user opens two browser windows. What would be better solution for this task?
My Item component:
export class FindClientItem extends React.PureComponent {
constructor() {
super();
this.state = {
modalIsOpen: false,
};
this.openModal = this.openModal.bind(this);
this.closeModal = this.closeModal.bind(this);
}
openModal() {
this.setState({ modalIsOpen: true });
}
closeModal() {
this.setState({ modalIsOpen: false });
localStorage.removeItem('itemId');
}
render() {
const { item } = this.props;
if(this.state.modalIsOpen){
localStorage.setItem('itemId',item.itemId);
}
// Put together the content of the repository
const content = (
<Wrapper>
<h3>{item.title}</h3>
Details: {item.description}...<button onClick={this.openModal}>
More
</button>
<Modal
isOpen={this.state.modalIsOpen}
onRequestClose={this.closeModal}
style={customStyles}
contentLabel="Modal"
>
<h3>{item.title}</h3>
Details: {item.description} <br />
<button onClick={this.openBidModal}>Submit</button>{' '}
</Modal>
</Wrapper>
);
// Render the content into a list item
return <ListItem key={`items-${item.itemId}`} item={content} />;
}
}
And then my other 1st Form component's saga:
export function* submitForm() {
try {
const formType = 'item';
const body = yield select(makeSelectModifiedData());
body.itemId = localStorage.getItem('itemId');
let requestURL;
switch (formType) {
case 'item':
requestURL = 'http://localhost:1234/item';
break;
default:
}
const response = yield call(request, requestURL, { method: 'POST', body });
} catch (error) {
Alert.error('Error message...', {
html: false,
});
}
}
Not sure if this is the "Best" way to do this, however, works well for me. Have you tried creating a shared js file (imported into both components) which GETS / SETS a variable? for example.
shared.js
let data = null;
setData(d){
data = d;
}
getData(){
return data;
}
addChangeListner(eventName, callback){
this.on(eventname, callback);
}
dispatcherCallback(action){
switch(action.actionType){
case 'SET_DATA':
this.getData();
}
}
Whenever you require your component to update, you can add an change listener to then return the new data once set so the components aren't out of sync. Just remember to remove the listener afterwords!
Component
componentDidMount(){
shared.addChangeListner('SET_DATA', this.onUpdate)
}
// use x to pass to your saga...
onUpdate(){
var x = shared.getData();
}
Hope this helps!
index.js
import {handleSave, loadData } from './action';
import Modal from './Modal',
export class GetFormData extends React.PureComponent {
componentDidMount() {
this.props.loadData();
}
saveData = (data) => {
this.props.handleSave(data)
}
render() {
return (
<div>
<Modal
isOpen={this.state.modalIsOpen}
onRequestClose={this.closeModal}
style={customStyles}
contentLabel="Modal"
data={this.props.getdata}
handlePost={this.saveData}
/>
</div>
)
}
}
const mapStateToProps = state => ({
getdata: state.formData,
});
const mapDispatchToProps = dispatch => ({
loadData: bindActionCreators(loadData, dispatch),
handleSave: bindActionCreators(handleSave, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(GetFormData);
actions.js
import {
LOAD_DATA,
LOAD_DATA_SUCCESS,
LOAD_DATA_FAILED
HANDLE_SAVE,
HANDLE_SAVE_SUCCESS,
HANDLE_SAVE_FAILED
} from './constants';
export function loadData() {
return {
type: LOAD_DATA,
};
}
export function loadDataSuccess(formData) {
return {
type: LOAD_DATA_SUCCESS,
formData
};
}
export function loadDataFailed(error) {
return {
type: LOAD_DATA_FAILED,
error
};
}
export function handleSave(data) {
return {
type: HANDLE_SAVE,
data
};
}
export function handleSaveSuccess() {
return {
type: HANDLE_SAVE_SUCCESS
};
}
export function handleSaveFailed(error) {
return {
type: HANDLE_SAVE_FAILED,
error
};
}
reducers.js
import { fromJS } from 'immutable';
import {
LOAD_DATA, LOAD_DATA_SUCCESS, LOAD_DATA_FAILED,
HANDLE_SAVE,
HANDLE_SAVE_SUCCESS,
HANDLE_SAVE_FAILED
} from './constants';
const initialState = fromJS({
formData: undefined,
});
function formDataReducer(state = initialState, action) {
switch (action.type) {
case LOAD_DATA:
return state;
case LOAD_DATA_SUCCESS:
return state.set('formData', action.formData);
case LOAD_DATA_FAILED:
return state.set('errormsg', fromJS(action.errormsg));
case HANDLE_SAVE:
return state.set('data', action.data);
case HANDLE_SAVE_SUCCESS:
return state.set('message', action.message);
case HANDLE_SAVE_FAILED:
return state.set('errormsg', fromJS(action.errormsg));
default:
return state;
}
}
saga.js
import { takeEvery, call, put } from 'redux-saga/effects';
import {
LOAD_DATA,
LOAD_DATA_SUCCESS,
LOAD_DATA_FAILED,
HANDLE_SAVE,
HANDLE_SAVE_SUCCESS,
HANDLE_SAVE_FAILED
} from './constants';
export function* getFormDataWorker() {
try {
const formData = yield call(api);
if (formData) {
yield put({ type: LOAD_DATA_SUCCESS, formData });
}
} catch (errormsg) {
yield put({ type: LOAD_DATA_FAILED, errormsg });
}
}
// watcher
export function* formDataWatcher() {
yield takeEvery(LOAD_DATA, getFormDataWorker);
}
export function* saveDataWorker(action) {
try {
const message = yield call(savedata, action.data);
if (message) {
yield put({ type: HANDLE_SAVE_SUCCESS, message });
}
} catch (errormsg) {
yield put({ type: HANDLE_SAVE_FAILED, errormsg });
}
}
// watcher
export function* saveDataWatcher() {
yield takeEvery(HANDLE_SAVE, saveDataWorker);
}
// All sagas to be loaded
export default [
saveDataWatcher,
formDataWatcher,
];
Modal.js
const Modal = ({data, handlePost}) => (
{ data ? data.map(item => (
<input type="text" value={item.id} />
)
}
<Button type="submit" onClick={handlePost}/ >
)
Hope this helps!
I would suggest the following:
Remove the usage of localstorage
On componentDidUpdate dispatch an action that sets the itemId in the Redux store.
componentDidUpdate() {
this.props.setItemId({itemId: this.props.item.itemId})
}
On form submit, dispatch the same action as you are currently using to trigger the saga.
Change your makeSelectModifiedData selector to return the itemId you are storing in Redux now.
After a bit of trial and error I finally manage to get my action creator working properly and passing the data I wanted into my redux store. Until now I've been dispatching it "manually" like this store.dispatch(fetchTest()); but It would be great if could use these data into a component.
So here is my action creator :
export const fetchTest = () => (dispatch) => {
dispatch({
type: 'FETCH_DATA_REQUEST',
isFetching:true,
error:null
});
return axios.get('http://localhost:3000/authors')
.then(data => {
dispatch({
type: 'FETCH_DATA_SUCCESS',
isFetching:false,
data: data
});
})
.catch(err => {
dispatch({
ype: 'FETCH_DATA_FAILURE',
isFetching:false,
error:err
});
console.error("Failure: ", err);
});
};
Here is my reducer :
const initialState = {data:null,isFetching: false,error:null};
export const ThunkData = (state = initialState, action)=>{
switch (action.type) {
case 'FETCH_DATA_REQUEST':
case 'FETCH_DATA_FAILURE':
return { ...state, isFetching: action.isFetching, error: action.error };
case 'FETCH_DATA_SUCCESS':
return Object.assign({}, state, {data: action.data, isFetching: action.isFetching,
error: null });
default:return state;
}
};
So far everything is working properly when using store.dispatch(fetchTest());.
Based on this example I tried to build the following component :
class asyncL extends React.Component {
constructor(props) {
super(props);
}
componentWillMount() {
this.props.fetchTest(this.props.thunkData)
// got an error here : "fetchTest is not a function"
}
render() {
if (this.props.isFetching) {
return console.log("fetching!")
}else if (this.props.error) {
return <div>ERROR {this.props.error}</div>
}else {
return <p>{ this.props.data }</p>
}
}
}
const mapStateToProps = (state) => {
return {
isFetching: state.ThunkData.isFetching,
data: state.ThunkData.data.data,
error: state.ThunkData.error,
};
};
const AsyncList = connect(mapStateToProps)(asyncL);
export default AsyncList
It doesn't work, I have an error on the componentWillMount() and probably somewhere else.
Also my data structure is kind of weird. To actually get to the data array I have to do state.ThunkData.data.data. The first data object is full of useless stuff like request, headers, etc...
So how should I write this component so I can at least passed the Async data into a console.log.
Thanks.
You need to mapDispatchToProps as well.
import { fetchTest } from './myFetchActionFileHere';
import { bindActionCreators } from 'redux';
function mapDispatchToProps(dispatch) {
return {
fetchTest: bindActionCreators(fetchTest, dispatch)
};
}
const AsyncList = connect(mapStateToProps, mapDispatchToProps)(asyncL);
export default AsyncList
documentation link: http://redux.js.org/docs/api/bindActionCreators.html
I am calling an async function in componentDidMount(), I expect after the state got updated with fetched data, the component should re-render, but no.
component code:
function mapStateToProps(state){
return {
posts: state.posts
}
}
function mapDispatchToProps(dispatch){
return bindActionCreators(actionCreators, dispatch)
}
export default class Main extends React.Component{
constructor(props){
super(props)
}
componentDidMount(){
this.fetchData()
}
fetchData(){
this.props.getAllPosts().then(() => {
console.log('props: ' + JSON.stringify(this.props))
this.props.posts.data.map( post => {
console.log(post.content)
})
})
}
render(){
return(
<div>
{!this.props.loaded
? <h1>loading...</h1>
:
<div>
{this.props.posts.data.map(post => {
return(
<div>
<h2>{post.title}</h2>
<p>{post.content}</p>
</div>
)
})}
</div>
}
</div>
)
}
}
const Home = connect(mapStateToProps, mapDispatchToProps)(Main)
action:
export function fetchAllPosts(){
return{
type: 'FETCH_ALL_POSTS'
}
}
export function receivedAllPosts(posts){
return{
type: 'RECEIVED_ALL_POSTS',
post_list: posts
}
}
export function getAllPosts(){
return (dispatch) => {
dispatch(fetchAllPosts())
return fetch('/api/posts')
.then(response => response.json())
.then(json => {
dispatch(receivedAllPosts(json.data))
})
.catch(error => {
})
}
}
reducer:
export function posts(state = {loaded: false}, action){
switch(action.type){
case 'FETCH_ALL_POSTS':
return Object.assign({}, state, {
'loaded': false
})
case 'RECEIVED_ALL_POSTS':
return Object.assign({}, state, {
'data': action.post_list,
'loaded': true
})
default:
return state
}
}
in the console.log() in the componentDidMount(), I do see the data got fetched, so it means it is in the state, but not applied into the render(), i don't know why.
It is because of a simple reason: you should use this.props.posts.loaded, instead of this.props.loaded.
When you set your state to props:
function mapStateToProps(state){
return {
posts: state.posts
}
}
Here state.posts is actually the object from your reducer:
{
'data': action.post_list,
'loaded': true
}
So similar to use access your posts list via this.props.posts.data, you should use this.props.posts.loaded. I believe you can debug through debugger or console.log easily.
A live code: JSFiddle
If you're using multiple reducers with a root reducer, then you should also provide your reducer's name to the mapStateToProps function.
e.g:
rootreducer.js:
import posts from './posts';
const rootReducer = combineReducers({ posts });
component:
function mapStateToProps(state){
return {
posts: state.posts.posts
}
}
My goal is to basically do a basic GET request in react-redux. I know how to do it with POST but not with GET because there is no event that is triggering the action.
Heres' the code for action
export function getCourses() {
return (dispatch) => {
return fetch('/courses', {
method: 'get',
headers: { 'Content-Type': 'application/json' },
}).then((response) => {
if (response.ok) {
return response.json().then((json) => {
dispatch({
type: 'GET_COURSES',
courses: json.courses
});
})
}
});
}
}
Where do i trigger this to get the data? in component?
import React from 'react';
import { Link } from 'react-router';
import { connect } from 'react-redux';
import { getCourses } from '../actions/course';
class Course extends React.Component {
componentDidMount() {
this.props.onGetCourses();
}
allCourses() {
console.log(this.props.onGetCourses());
return this.props.courses.map((course) => {
return(
<li>{ course.name }</li>
);
});
return this.props
}
render() {
return (
<div>
<ul>
{ this.allCourses() }
</ul>
</div>
)
}
}
const mapStateToProps = (state) => {
return {
courses: state.course.courses
}
}
const mapDispatchToProps = (dispatch) => {
return {
onGetCourses: () => dispatch(getCourses)
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Course);
I tried this but it doesn't work.
Course Reducer
const initialState = {
courses: []
};
export default function course(state= initialState, action) {
switch (action.type) {
case 'GET_COURSES':
return Object.assign({}, state, {
courses: action.courses
})
default:
return state;
}
}
First, onGetCourses: () => dispatch(getCourses) should be changed to onGetCourses: () => dispatch(getCourses()) (you need to actually invoke the action creator).
When it comes to where you should call the action, it is absolutely fine to do it in componentDidMount, as you have done.
In case you did not notice, you have two return's in your allCourses().
I have similar code in my codebase, but I don't use return in front of fetch and response.json() because the function should return action object.