I'm trying to use my first thunk in redux-thunk:
getting redux-thunk into my app which already uses redux-observable:
import thunk from 'redux-thunk'
export const store = createStore(
rootReducer,
vepo,
composeWithDevTools(applyMiddleware(createEpicMiddleware(rootEpic), thunk))
)
Using it like so:
Action creator:
export const updateShowProductIsVeganModal = (payload: boolean) => {
return function (dispatch) {
return new Promise((resolve, reject) => {
dispatch({
type: 'UPDATE_INSPECTION_SHOW_PRODUCT_IS_VEGAN_MODAL',
payload
})
resolve()
})
}
}
Then I have this component (It's much bigger, I've stripped it down for this question):
const veganPress = (props, refs) => {
props.updateShowProductIsVeganModal(true).then(() => {
console.log("plz work")
refs.toast.show('hello world!')
})
}
class ProductDetailsView extends Component<any, State> {
render = () => {
return (
<Button onPress={() => veganPress(this.props, this.refs)}>
<Toast ref="toast"/>
)}
}
const mapDispatchToProps = (dispatch: Dispatch<*>): any => ({
updateShowProductIsVeganModal: (show: boolean) => {
dispatch(updateShowProductIsVeganModal(show))
}
})
const view = connect(
mapStateToProps,
mapDispatchToProps
)(ProductDetailsView)
I get the error:
ExceptionsManager.js:63 Cannot read property 'then' of undefined
It is referring to the .then inside veganPress()
How can return a promise that can use .then in the way I am trying to do it?
updateShowProductIsVeganModal doesnt return promise. Change it to
const mapDispatchToProps = (dispatch: Dispatch<*>): any => ({
updateShowProductIsVeganModal: (show: boolean) => {
return dispatch(updateShowProductIsVeganModal(show))
}
})
Related
I am working on an e-commerce shopping cart app. I am not able to use getState() method to access the store.
This is my code from actions/cartActions.js file that is giving me the error:
export const removeFromCart = (product) => (dispatch, getState) => {
const cartItems = getState()
.cart.cartItems.slice()
.filter((x) => x._id !== product._id);
dispatch({ type: REMOVE_FROM_CART, payload: { cartItems } });
localStorage.setItem("cartItems", JSON.stringify(cartItems));
};
From OP's comment I guess OP want to achieve something like this:
function Cart(props) {
const { cartItems, removeFromCart } = props
return (<div>
<h1>Cart</h1>
{cartItems.map(product =>
<div key={product._id}>
<div>{product.name}</div>
{/* how you'd invoke removeFromCart đŸ‘‡ */}
<button onClick={() => removeFromCart(product)}>Delete</button>
</div>
)}
</div>)
}
And you want to achieve this through react-redux's connect(). It's feasible, but not in the way you currently write your code.
Let's revisit the doc first:
connect() Parameters​
connect accepts four different parameters, all optional. By convention, they are called:
mapStateToProps?: (state, ownProps?) => Object
mapDispatchToProps?: Object | (dispatch, ownProps?) => Object
mergeProps?: (stateProps, dispatchProps, ownProps) => Object
options?: Object
We need state and dispatch in one place in order to create removeFromCart. Reality is, in mapStateToProps we have access to state, in mapDispatchToProps we have access to dispatch, the only place we can access both is within the 3rd param, mergeProps function.
mergeProps should be specified with maximum of three parameters. They are the result of mapStateToProps(), mapDispatchToProps(), and the wrapper component's props, respectively.
This brings us to the solution:
export default connect(
state => ({ state }), // simply pass down `state` object
dispatch => ({ dispatch }), // simply pass down `dispatch` function
// here we do the real job:
({ state }, { dispatch }) => {
const removeFromCart = (product) => {
const cartItems = state.cart.cartItems.slice()
.filter((x) => x._id !== product._id);
dispatch({ type: REMOVE_FROM_CART, payload: { cartItems } });
localStorage.setItem("cartItems", JSON.stringify(cartItems));
};
return {
cartItems: state.cart.cartItems,
removeFromCart,
}
}
)(Cart)
I am quite used to class components. There I can pass a function that is bound to this to other components without problems. I thought the same would hold true for functional components.
Yet, the following code simply does not work:
const Dropdown: React.FC<{onNewPost: any}> = (props) => {
return(
<div onClick={props.onNewPost}></div>
)
}
function AddMessage(props: IProps) {
const { conversationUUID } = props.match.params;
const navigateToNewPost = (postUUID: string) => {
props.history.push(`/app/messages/new/${postUUID}/`)
}
const onNewPost = () => {
// props.history.push('/example/') -> this works without problems
props.createPost(
conversationUUID,
navigateToNewPost
)
}
return(
<Dropdown onNewPost={onNewPost}/>}
)
}
const mapStateToProps = (state: AppState) => ({
})
const mapDispatchToProps = (dispatch: Dispatch) => bindActionCreators({
createPost
}, dispatch)
export default connect(mapStateToProps, mapDispatchToProps)(withRouter(AddMessage));
I think the problem arises because props.history.push is not bound to anything here but the global object? I think I am having trouble understanding how functions are bound in functional components ... Maybe some helpful soul can come up with an explanation. Thanks!
EDIT:
export const createPost = (conversationUUID: string, callback: any) => async (dispatch: Dispatch) => {
try {
const res: any = authAxios.post('/posts/', {
conversation: conversationUUID,
})
if (callback) {callback(res.data.uuid)}
} catch(e) {
}
}
I've had a great deal of frustration lately when I was trying to use axios to fetch data from a localhost withing a React/Redux bookstore project.The reason why I used axios is to connect the App to a rails api(backend) and try to update the store from the DB whenever it is needed(namely the list of books). However, when I try to pass the response.data(array of books) of the api axios call to the Action Creator I get a Promise in the React component.
Book List Component
import React from 'react';
import { connect } from 'react-redux';
import axios from 'axios';
import Book from '../components/book';
import { removeBookAction, getBooksAction } from '../actions/index';
class BookList extends React.Component {
UNSAFE_componentWillMount() {
this.props.getBooks();
}
render() {
const { books, handleRemoveBook } = this.props;
console.log(books) // prints Promise {<resolved>: Array(12)} :(
return (
<tbody>
{books.length > 0 ? books.map((book) => (
<Book key={book.id} item={book} handleRemoval={handleRemoveBook} />
))
: (
<tr>
<td>Empty List</td>
</tr>
)}
</tbody>
);
}
}
const displayableBooks = (books, theFilter) => {
//console.log(books)
if (theFilter === 'All') {
return books;
}
return books.filter(item => item.category === theFilter);
};
// inject store state as props to Booklist component
const mapStateToProps = (state) => {
return {
books: displayableBooks(state.books, state.filter),
};
};
const mapDispatchToProps = (dispatch) => ({
handleRemoveBook: (book) => {
dispatch(removeBookAction(book));
},
getBooks: () => axios.get('api/v1/books').then(response => {
dispatch(getBooksAction(response.data));
}),
});
BookList = connect(mapStateToProps, mapDispatchToProps)(BookList);
export default BookList;
Books Action creators
const creatBookAction = (book) => {
return {
type: 'CREATE_BOOK',
book,
}
};
const removeBookAction = (book) => ({
type: 'REMOVE_BOOK',
id: book.id,
});
const getBooksAction = (books) => {
return {
type: 'GET_BOOKS',
books,
};
}
const changeFilterAction = (filter) => ({
type: 'CHANGE_FILTER',
filter,
});
export { creatBookAction, removeBookAction, changeFilterAction, getBooksAction };
Books Reducer
const CREATE_BOOK = 'CREATE_BOOK';
const REMOVE_BOOK = 'REMOVE_BOOK';
const GET_BOOKS = 'GET_BOOKS';
const booksReducer = async (state = [], action) => {
switch (action.type) {
case CREATE_BOOK:
return [
...state,
{ ...action.book },
];
case REMOVE_BOOK:
const index = state.findIndex((value) => value.id === action.id);
return state.slice(0, index).concat(state.slice(index + 1));
case GET_BOOKS:
return action.books;
default:
return state;
}
};
export default booksReducer;
As you can see the code above, I am trying to create some sort of synchronization between the Redux store and the DB, but I am stuck at the fist step (i.e getting the data). I am a beginner with React/Redux and axios, so please consider explaining other alternatives if my approach (which is a combination of other approaches mentioned in tutorials) is inefficient of impossible to apply. Thanks :)
I realized that I made a grave mistake. The reason why I am getting a promise instead of the actual data is because of my books reducer. I made it an asynchronous function which I suppose will inevitably return a promise.
You should use a middleware to make async call action like redux thunk
const mapDispatchToProps = (dispatch) => ({
handleRemoveBook: (book) => {
dispatch(removeBookAction(book));
},
getBooks: () => dispatch(fetchBooksAction());
});
Here your async action
const fetchBooksAction = () => (dispatch) => {
axios.get('api/v1/books').then(response => {
dispatch(getBooksAction(response.data);
});
}
To clearify I'm pretty newbie with the concept of react-redux. I try to dispatch an async action in the presentational comp. but this does not seem to work out.
Container Component
const store = configureStore();
const Root: React.FC = () => (
<Provider store={store}>
<App />
</Provider>
);
render(<Root/>, document.getElementById('root'));
Presentational Component
interface AppProps {
system: SystemState,
updateSession: typeof updateSession,
getLanguageThunk: any
}
const App: React.FC<AppProps> = ({system, updateSession, getLanguageThunk}) => {
useEffect(() => {
getLanguageThunk().then((res: any) => {
console.log(res);
i18n.init().then(
() => i18n.changeLanguage(res.language));
});
}, []
);
return (
<div>
<div className="app">
<TabBar/>
</div>
</div>
);
};
const mapStateToProps = (state: AppState) => ({
system: state.system
});
export default connect(mapStateToProps, { updateSession, getLanguageThunk })(App);
But the console everytime logs undefined. So I am doint something wrong here. Maybe some of u can help me out on here.
Redux middleware
export const getLanguageThunk = (): ThunkAction<void, AppState, null, Action<string>> => async dispatch => {
const language = await getLanguage();
dispatch(
updateSession({
disableSwipe: false,
language
})
)
};
async function getLanguage() {
try {
const response = await fetch('http://localhost:3000/language');
return response.json();
} catch {
return { language: 'en_GB' }
}
}
You need to return the language from getLanguageThunk, to be able to use it from promise in the useEffect method
export const getLanguageThunk = (): ThunkAction<void, AppState, null, Action<string>> => async dispatch => {
const language = await getLanguage();
dispatch(
updateSession({
disableSwipe: false,
language
})
)
return language;
};
A child component has the following button code:
// SelectDonation.js
<button
onClick={(e) => {
e.preventDefault();
this.props.testThunk();
console.log(store.getState());
}}
>Test thunks</button>
this.props.testThunk() does not update the state object. I connected Redux Thunk like so:
// reducer.js
import ReduxThunk from "redux-thunk";
const starting_state = {
log_to_console : 0,
donation_amount : 12,
checkoutStep : 'selectDonation',
};
const reducer = (previous_state = starting_state, action) => {
switch (action.type) {
case 'thunkTest':
return {
...previous_state,
redux_thunk_test_var : action.payload
};
default:
return previous_state;
}
};
export default createStore(reducer, starting_state, applyMiddleware(ReduxThunk));
I expect a new state property redux_thunk_test_var to display in state but it does not onClick. I do see the state variables with initial states in the console though.
Am I not passing down the thunk correctly? Here is App.js
// App.js
{this.props.checkoutStep === checkoutSteps.selectDonation &&
<SelectDonation
dispatch_set_donation_amount = {this.props.dispatch_set_donation_amount}
dispatchChangeCheckoutStep={this.props.dispatchChangeCheckoutStep}
{...this.props}
/>
}
</Modal>
</header>
</div>
);
}
}
const map_state_to_props = (state) => {
return {
log_prop : state.log_to_console,
donation_amount : state.donation_amount,
checkoutStep : state.checkoutStep,
}
};
const map_dispatch_to_props = (dispatch, own_props) => {
return {
dispatch_set_donation_amount : amount => dispatch(set_donation_amount(amount)),
dispatchChangeCheckoutStep : newStep => dispatch(changeCheckoutStep(newStep)),
dispatchUpdateStateData : (stateData, stateVariable) => (dispatch(updateStateData(stateData, stateVariable))),
testThunk
}
};
The action thunk:
// actions.js
export const testThunk = () => {
const testDelay = setTimeout(() => 'Set Timeout done', 2000);
return (dispatch) => {
testDelay.then((data) => dispatch({
type: 'thunkTest',
payload: data })
)
}
};
You need to dispatch the result of the testThunk() action creator. Right now, you're just returning it, and not calling dispatch(testThunk()).
See this gist comparing syntaxes for dispatching to help understand the issue better.
The best way to fix this is to use the "object shorthand" form of mapDispatch. As part of that, I suggest changing the prop names to remove the word "dispatch", which lets you use the simpler ES6 object literal syntax:
const map_dispatch_to_props = {
set_donation_amount,
changeCheckoutStep,
updateStateData,
testThunk,
};
conponentDidMount() {
this.props.testThunk();
}
const map_dispatch_props = {
testThunk
}
//action creator
const fetch = (data) => ({
type: 'thunkTest',
payload: data
})
const fakeFetch = () => new Promise((resolve, reject) => setTimeout(() => resolve('Set Timeout done'), 2000));
export const testThunk = () => (dispatch) => fakeFetch.then(data => dispatch(fetch(data)))