I am trying to adopt the philosophy of performing all operations in 'Container Component' (aka smart component) & then just passing data on to the 'Presentation Components'.
I am stuck at this point, where I need to validate the user action (event fired) before I dispatch the action to the Reducer. The way I want to do this is by validating the event in the function inside 'mapDispatchToProps'.
The code looks like this:
const mapStateToProps = ({ oneState, twoState }) => {
return({
oneState : oneState,
twoState : twoState
});
};
const mapDispatchToProps = ( dispatch ) => {
return({
dispatchOneAction : () => {
// do the validation here. The validation requires access to
// the 'oneState' obj above in the 'mapStateToProps'
}
});
};
const C_Element = connect( mapStateToProps, mapDispatchToProps )( Ele );
My question is, is it possible? Or must I perform the validation downstream in the presentation component and then call the 'dispatchOneAction' function?
The connect allows a 3rd argument called mergeProps:
connect([mapStateToProps], [mapDispatchToProps], [mergeProps],
[options])
mergeProps is a function that will receive the result from your mapStateToProps, mapDispatchToProps and the props provided to your component. It allows you to use them all in order to manipulate and return the final props that should be applied to your component. This could be an opportunity to decorate your action creators with additional validation logic based on your state. You can do whatever you like, returning a completely new set of props to be applied to your component.
For example, using your described case:
const mapStateToProps = ({ oneState, twoState }) => {
return({
oneState : oneState,
twoState : twoState
});
};
const mapDispatchToProps = ( dispatch ) => {
return bindActionCreators({
successAction: MyActions.successAction,
failAction: MyActions.failAction
}, dispatch);
};
const mergeProps = (stateProps, dispatchProps, ownProps) => {
const { oneState, twoState } = stateProps;
const { successAction, failAction } = dispatchProps;
const validatorAction = () => {
if (oneState && twoState) {
successAction();
} else {
failAction();
}
}
return Object.assign(
{},
stateProps,
// We are providing new actions props
{ validatorAction },
ownProps
);
}
const C_Element = connect( mapStateToProps, mapDispatchToProps, mergeProps)( Ele);
Refer to the official react-redux docs for more info.
An alternative approach is to use redux-thunk based actions. This allows you to encapsulate logic within your action creators with access to the state. You can additionally fire off further actions from within your thunk action.
For example:
function validatingAction() {
return (dispatch, getState) => {
const { stateOne, stateTwo } = getState();
if (stateOne && stateTwo) {
dispatch(successAction());
}
dispatch(failedAction());
};
One of the main benefits of separating "Containers" and "Presentational Components" is to handle all logic for a specific component inside it. That said, you might define an action that changes your state and only fire it when valid.
Class PresentationalComponent extends React.Component {
...
onEventHandler() {
if ( eventIsValid ) this.props.changeState();
}
...
}
And:
Class ContainerComponent extends React.Component {
...
render() {
<PresentationalComponent changeState={actions.changeState} />
}
}
Related
Hi I'm very new to React and this is my first project, what I'm trying to do is when onClick, the code will fetch data from API using Axios and display the object.
First, I tried using a reducer and failed. Read on Redux docs, that the way to resolve when dealing with asynchronous actions we to use applymiddleware. So after trying and adjusting I end up with:
// I KNOW... YOU MIGHT BE THINKING WHY REDUX FOR SUCH A SIMPLE APP, WHY NOT JUST USE REACT?? IT'S SUCH AN OVER-KILL. WELL... I FORCE MYSELF TO IMPLEMENT IT ON THE FIRST PROJECT TO UNDERSTAND AND DEMONSTRATE THE FUNCTIONALITY OF REACT, REDUX AND REACT-REDUX. SIMPLE APP IS IT CLEARER/OBVIOUS.
const thunk = ReduxThunk.default;
const { Provider, connect } = ReactRedux;
const { createStore, applyMiddleware } = Redux;
const GENERATE = 'GENERATE';
class QuoteGenerator extends React.Component {
generateQuote = () => {
this.props.dispatch(generateNewQuote());
};
generate = () => {
this.props.dispatch({
type: GENERATE,
});
};
render() {
console.log(this.props);
return (
<div>
<h1>{this.props.quote}</h1>
<button onClick={this.generate}>Generate</button>
</div>
);
}
}
const mapStateToProps = (state) => ({
quote: state.quote,
});
const Container = connect(mapStateToProps)(QuoteGenerator);
/**************************************************************/
const initState = {
quote: 'Generate Me!',
};
function fetchQuote() {
return axios.get(
'https://gist.githubusercontent.com/camperbot/5a022b72e96c4c9585c32bf6a75f62d9/raw/e3c6895ce42069f0ee7e991229064f167fe8ccdc/quotes.json'
);
}
function returnQuote(quote) {
return {
quote: quote,
};
}
// But what do you do when you need to start an asynchronous action,
// such as an API call, or a router transition?
// Meet thunks.
// A thunk is a function that returns a function.
// This is a thunk.
function generateNewQuote(state = initState, action) {
// if (action.type === GENERATE) {
// console.log("It Works")
// Invert control!
// Return a function that accepts `dispatch` so we can dispatch later.
// Thunk middleware knows how to turn thunk async actions into actions.
return function (dispatch) {
return fetchQuote().then((res) => {
const selectRandomQuote = Math.floor(
Math.random() * res.data.quotes.length
);
const quoteObj = {
quote: res.data.quotes[selectRandomQuote]['quote'],
author: res.data.quotes[selectRandomQuote]['author'],
};
console.log({ quote: res.data.quotes[selectRandomQuote]['quote'] });
return { quote: res.data.quotes[selectRandomQuote]['quote'] };
});
};
// } else {
// return state;
// }
}
function reducer(state = initState, action) {
if (action.type === GENERATE) {
return {
quote: 'It Works!',
};
} else {
return state;
}
}
// applyMiddleware supercharges createStore with middleware:
const store = createStore(generateNewQuote, applyMiddleware(thunk));
class App extends React.Component {
render() {
return (
<Provider store={store}>
<Container />
</Provider>
);
}
}
ReactDOM.render(<App />, document.getElementById('root'));
I'm able to trigger the function and console log the output but with no luck, it's not showing...
I really appreciate the guidance to share some light on this, been really keen on React but the first has hit me hard. Thanks in advance, cheers!
I set some values in React Redux store while the ComponentDidMount() function. Redux Dev Tools displays that the state has been updated. But in props It doesn't get changed.
My reducer.js is,
const inititalState = {
slickColumns: null,
slickData: null
}
const reducer = (state = inititalState, actions) => {
switch(actions.type) {
case actionsTypes.SET_SLICK_GRID_COLUMNS: {
return {
...state,
slickColumns: columns
};
}
case actionsTypes.SET_SLICK_GRID_DATA: {
return {
...state,
slickData: [...mock_slick_data]
};
}
default: {
return state;
}
}
}
export default reducer;
action.js,
import * as actions from './actions';
export const setSlickGridColumns = () => {
return {
type:actions.SET_SLICK_GRID_COLUMNS
}
}
export const setSlickGridData = () => {
return {
type: actions.SET_SLICK_GRID_DATA
}
}
main.js, (Mapping Redux to state)
const mapStateToProps = state => {
return {
slickColumns: state.slickColumns,
slickData: state.slickData
};
};
const mapDispatchToProps = dispatch => {
return {
onSetSlickDataColumns: () => {
dispatch(actionTypes.setSlickGridColumns());
},
onSetSlickData: () => {
dispatch(actionTypes.setSlickGridData());
}
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Dimensions()(Home));
in ComponentDidMount function,
this.props.onSetSlickDataColumns(); // able to see this method is running and the state is updated in Redux Dev Tools
this.props.onSetSlickData();
console.log(this.props.slickColumns); // getting null here.
dv.setItems(this.props.slickData);
Even thought the state is updated in store, I am still not able to get the data in the props? why? any ideas?
index.js,
import slickReducer from './store/reducers/SlickGrid';
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
slickReducer,
composeEnhancers(applyMiddleware(thunk))
);
const app = (
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>
);
ReactDOM.render(app, document.getElementById("root"));
[Edit]: Initially I set initialState object properties as 'null;
Adding my Redux Screenshot here,
Adding extra some logs in here. This might helpful to resolve this issue. Actually the grid instance is created in ComponentDidMount() method.
componentDidMount() {
console.log("[componentDidmount]....");
this.props.onSetSlickDataColumns();
this.props.onSetSlickData();
console.log(this.props);
if (this.props.slickColumns.length !== 0) {
const grid = (this.gridInstance = new Grid(
this.grid,
dv,
this.props.slickColumns,
options
));
// ------------------
}
}
When doing grid object, It should not have empty columns, and empty DataView.
I called the setSlickGridColumns and setSlickGridData method in various lifecycle methods such as constructor, componentWillMount and put console logs for props in mapStateToProps, constructor, componentWillMount, render and componentDidMount methods also. From the logs what I am getting is,
[mapStateToProps]
this.props.slickColumns: null
[componentConstructor].... calling onSetSlickDataColumns() and onSetSlickData() methods here..
this.props.slickColumns: null
[componentwillmount].... calling onSetSlickDataColumns() and onSetSlickData() methods here..
this.props.slickColumns: null
[render]
this.props.slickColumns: null
[componentDidmount].... calling onSetSlickDataColumns() and onSetSlickData() methods here..
this.props.slickColumns: null
[mapStateToProps]
this.props.slickColumns: Array(300) // Here props values are set
[render]
this.props.slickColumns: Array(300)
From the logs, what I understand is, The data has to be filled before the componentDidMount() method. But It doesn't setting up even though I dispatched reducer function in constructor and ComponentWillMount. Hope this logs help to resolve this problem.
Problem is you are not setting new data in your reducer you can see
const reducer = (state = inititalState, actions) => {
switch(actions.type) {
case actionsTypes.SET_SLICK_GRID_COLUMNS: {
return {
...state,
slickColumns : columns // you have to pass your new data as payload from your function : actions.payload
};
}
case actionsTypes.SET_SLICK_GRID_DATA: {
return {
...state,
slickData: [...mock_slick_data] // same here
};
}
default: {
return state;
}
}
}
You can pass your data when you dispatch an action
dispatch(actionTypes.setSlickGridColumns('Pass your data here '));
Then you can get your data as argument like
export const setSlickGridColumns = (data) => {
return {
type:actions.SET_SLICK_GRID_COLUMNS,
payload : data // pass your data as payload
}
}
Now you can use your data in reducer like actions.payload
.......
case actionsTypes.SET_SLICK_GRID_COLUMNS: {
return {
...state,
slickColumns : action.payload
};
........
try below code -> you need to return the dispatch inside the mapDispatchToProps like below
const mapDispatchToProps = dispatch => {
return {
onSetSlickDataColumns: () => {
return dispatch(actionTypes.setSlickGridColumns());
},
onSetSlickData: () => {
return dispatch(actionTypes.setSlickGridData());
}
};
};
Actually you're not going to get updated props in componentDidMount lifecycle hook, as you are dispatching your function after your component is mounted. You'll get updated props in your render, componentWillReceiveProps (deprecated), getDerivedStateFromProps and some other lifecycle hooks. You can read more about which lifecycle hooks are being called when props are updated in the official docs of react.
And then you're missing return statement in mapDispatchToProps as mentioned in one other answer.
I'm new to redux, and I can find lots of info on how to pass a redux state to the component, but not the other way round, so I'm not sure if I'm searching the correct vocabulary. But Essentially I want to be able to reference the current state of a react component in a redux helper function, this is what I've done - and I'm getting TypeError: dispatch is not a function and handleSubmit is just launching as soon as the page is loaded:
App.js
render() {
return (
<div className="App">
<p>{this.state.id}</p>
<form onSubmit={this.handleSubmit(this.state.id)}>
<button type="submit">Submit</button>
</form>
</div>
)
}
const mapDispatchToProps = dispath => bindActionCreators({
handleSubmit
}, dispath);
export default connect(
mapDispatchToProps
)(App);
reducers.js
export const handleSubmit = (test) => {
window.open("http://localhost:5000/"+test);
}
//Reducer
export default (state = [], action) => {
switch (action.type) {
default:
return state;
}
};
First, you don't use the function that react-redux pass through the props and try to call handleSubmit on the component itself.
You are also calling the function inside onSubmit immediately instead of passing a reference to a function so wrap it in an arrow function and use handleSubmit from this.props
onSubmit={() => this.props.handleSubmit(this.state.id)}
Second, the first argument to connect is the mapping function to get a slice of the state called mapStateTpProps by convention, pass in null as the first argument.
there is also no need to use bindActionCreators and you can just pass an object with functions and react-redux will wrap them in dispatch for you
export default connect(
null,
{ handleSubmit }
)(App);
You need to put id to the state of App and manage it through redux.
Code below will help you.
// App.js
render() {
return (
<div className="App">
<p>{this.props.id}</p>
<form onSubmit={this.props.ActionSubmit(this.props.id)}>
<button type="submit">Submit</button>
</form>
</div>
)
}
const mapStateToProps = store => ({
id: store.appReducer.id,
})
const mapDispatchToProps = dispath => bindActionCreators({
ActionSubmit
}, dispath);
export default connect(
mapStateToProp,
mapDispatchToProps
)(App);
// reducers.js
export ACTION_SUBMIT = 'ACTION_SUBMIT'
export const ActionSubmit = id => ({
type: ACTION_SUBMIT,
payload: {
id,
}
})
const initialState = {
id: 0,
}
const doSubmit = (id) => {
window.open("http://localhost:5000/"+id);
}
export default AppReducer(state = initialState, action) {
switch(action.type) {
case ACTION_SUBMIT:
doSubmit( action.payload.id)
return {
id: action.payload.id,
}
default:
return state
}
}
I have a component that makes an API call and then updates the state through a reducer. The problem is, this doesn't work so well cause the data don't get updated in the component, it's like the react didn't notice a state change a never re-rendered the component, but I'm not sure if that's the real issue here. So the component looks like this:
class MyComponent extends Component {
componentDidMount() {
// ajax call
this.props.loadData(1);
}
render() {
return (
<Grid>
<MySecondComponent
currentData={this.props.currentData}
/>
</Grid>
);
}
}
const mapStateToProps = state => ({
reducer state.myReducer,
currentData: state.myReducer.currentData
});
const mapDispatchToProps = dispatch => {
return {
loadData: () => {
HttpClient.getData(id, (data) => {
dispatch(
action_loadCurrentData(
data
)
);
});
},
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(MyComponent);
I am doing 2 things here: issuing an API call as soon as component is mounted, and then after data is fetched, dispatching action_loadCurrentData
This action looks like this:
//Action
export function action_loadCurrentData(
data
) {
return {
type: 'LOAD_CURRENT_DATA',
payload: {
currentData: data,
}
};
}
and the reducer:
//Reducer
const defaultState = {
};
const reducer = (state = defaultState, action) => {
switch (action.type) {
case 'LOAD_CURRENT_DATA':
state = {
...state,
currentData: {
myData: {
...state.currentData.myData,
0: action.payload.currentData
}
}
};
}
};
export default myReducer;
So the issue here is that the this.props.currentData that I'm passing to MySecondComponent will end up empty, as if I didn't set the data at all. However, If I stop the execution in the debugger and give it a few seconds, the data will be populated correctly, so I'm not sure what I'm doing wrong here?
Don't reassign state, return the newly created object instead
const reducer = (state = defaultState, action) => {
switch (action.type) {
case 'LOAD_CURRENT_DATA':
return {
...state,
currentData: {
myData: {
...state.currentData.myData,
0: action.payload.currentData
}
}
};
}
};
Your reducer needs to return the new state object, which needs to be a different instance from the previous state to trigger components update.
According to redux documentation:
The reducer is a pure function that takes the previous state and an action, and returns the next state.
And
Things you should never do inside a reducer:
Mutate its arguments;
Perform side effects like API calls and routing transitions;
Call non-pure functions, e.g. Date.now() or Math.random().
I have a Connector, which has mapDispatchToProps and mapStateToProps functions, and I need to dispatch a action from my main component.
I'm getting an error saying dispatch is not defined when I'm trying to dispatch fetchPlaces(this.props.user.id)
this.props.user.id has value 1.
I need to get the user id and pass it to fetchPlaces, which intern gets me the places of the user. I'm not sure how to do it.
Connector
const mapStateToProps = function (store) {
return {
elements: store.elements.elements,
places: store.places.places,
geocode : store.geocode.geocode,
user : store.user.user
};
};
const mapDispatchToProps = function (dispatch) {
return {
userAction : dispatch(fetchUser()),
elementsAction : dispatch(fetchCategories()),
placesAction: (id) => { dispatch(fetchPlaces(id)) }
}
}
class BasicinfoConnector extends React.Component{
render() {
console.log(this.props.user);
if(typeof this.props.user.id != "undefined"){
return (
<Basicinfo elements={this.props.elements} places={this.props.places} geocode={this.props.geocode} user={this.props.user}/>
);
}
else{
return null;
}
}
}
export default Redux.connect(mapStateToProps, mapDispatchToProps)(BasicinfoConnector);
Component :
componentWillMount() {
console.log(this.props);
console.log(this.props.user.id);
dispatch(fetchPlaces(this.props.user.id))
}
Is placesAction: (id) => { dispatch(fetchPlaces(id)) } the right syntax of doing it?
UPDATE
I changed componentWillMount :
componentWillMount() {
console.log(this.props);
console.log(this.props.user.id);
dispatch(this.props.placesAction(this.props.user.id))
}
and mapDispatchToProps :
const mapDispatchToProps = function (dispatch) {
return {
userAction: bindActionCreators(fetchUser, dispatch),
elementsAction: bindActionCreators(fetchUser, dispatch),
placesAction: (id) => { dispatch(fetchPlaces(id)) }
}
}
Still have the same error.
You need to pass the property down to the next level, either by sharing all your props like this:
<Basicinfo {...this.props} />
or only the particular ones that you want
<Basicinfo placesAction={(id) => this.props.placesAction(id)} />