I want to build some buttons (from react-bootstrap Buttons) that know when the mouse cursor has entered and left them. No problem with hooking this up, and I get everything to fire just fine. However, if I have several such buttons, and each gets a name passed down to it, how do I get the name into the reducer?
In the following example, I want bsName to be passed to the reducer, take a peek at mapsDispatchToProps:
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Button } from 'react-bootstrap';
const InteractiveButton = ({
bsStyle,
bsName,
bsText,
dispButtonEnter,
dispButtonLeave
}) => (
<div>
<Button
onMouseEnter={dispButtonEnter}
onMouseLeave={dispButtonLeave}
bsStyle={bsStyle}
bsSize="large"
title={bsName}
block
>
{bsText}
</Button>
</div>
);
InteractiveButton.propTypes = {
bsStyle: PropTypes.string.isRequired,
bsName: PropTypes.string.isRequired,
bsText: PropTypes.string.isRequired,
dispButtonEnter: PropTypes.func.isRequired,
dispButtonLeave: PropTypes.func.isRequired
};
const dispButtonEnter = { type: 'BUTTON_MOUSE_ENTER' };
const dispButtonLeave = { type: 'BUTTON_MOUSE_LEAVE' };
function mapStateToProps() {
return {};
}
function mapDispatchToProps(dispatch) {
return {
dispButtonEnter: e => dispatch({
...dispButtonEnter,
buttonName: bsName <<<<<<<<<<<<<<<<<<<<<<<< something like this, but functioning
buttonEnterTarget: e.relatedTarget
}),
dispButtonLeave: e => dispatch({
...dispButtonLeave,
buttonName: bsName <<<<<<<<<<<<<<<<<<<<<<<< something like this, but functioning
buttonLeaveTarget: e.relatedTarget
})
};
}
export default connect(mapStateToProps, mapDispatchToProps)(InteractiveButton);
You should utilize closures properly here don't just dispatch directly instead make the call to the closure method that will receive the the name of button and then add that name in the payload.
closureFunction(btnName){
dispatch({
...dispButtonEnter,
payload:{name: btnName}
})
}
In reducer you will be able to access the payload in action argument.
Related
I have a redux-toolkit store at store.js.
import { configureStore } from '#reduxjs/toolkit';
import productCurrency from './stateSlices/productCurrency';
const Store = configureStore({
reducer: {
productCurrency: productCurrency,
},
})
export default Store;
The createslice() function itself is in a different file and looks like this below.
import { createSlice } from '#reduxjs/toolkit'
const initialState = {
value: '$',
}
export const productCurrency = createSlice({
name: 'productCurrency',
initialState,
reducers: {
setproductCurrency(state, newState) {
state.value = newState
},
},
})
export const { setproductCurrency } = productCurrency.actions;
export default productCurrency.reducer;
My issue is that I have a class component NavSection that needs to access the initial state and the reducer action setproductCurrency() to change the state. I am trying to use the react-redux connect() function to accomplish that.
const mapStateToProps = (state) => {
const { productCurrency } = state
return { productCurrency: productCurrency.value }
}
const mapDispatchToProps = (dispatch) => {
return {
setproductCurrency: () => dispatch(setproductCurrency()),
dispatch,
}
}
export default connect(mapStateToProps, mapDispatchToProps)(NavSection);
Now, I am able to access the state by ussing this.props.productCurrency. Yet, if I try to access the setproductCurrency() by ussing this.props.setproductCurrency()... Chrome console gives me an error that "this.props.setproductCurrency() is not a function".
Is there a way of fixing this, or am I trying to do something impossible?
UPDATE #1
I think I just moved the ball in the right direction. I changed the onClick function to be an arrow function as shown below.
onClick={() => this.props.setproductCurrency('A$')}
Now, setproductCurrency() is considered a function, but it returns a different error when I click the button...
Objects are not valid as a React child (found: object with keys {type, payload}). If you meant to render a collection of children, use an array instead.
Why would this function now return an object? It is supposed to change the state and trigger a re-render of the page so that the class component can access the newly changed state.
To be clear, RTK has nothing to do with React-Redux, connect, or mapDispatch :)
The current error of "Objects are not valid as a React child" is because your reducer is wrong. A reducer's signature is not (state, newState). It's (state, action). So, your line state.value = newStateis reallystate.value = action`, and that's assigning the entire Redux action object as a value into the state. That's definitely not correct conceptually.
Instead, you need state.value = action.payload.
I'm writing a website in react which displays information on mobile apps and I'm using redux to store the information about the current app. I have an input text field located in the header where the user can type for some app id and if it's a valid id they will be redirected to another page, if the id is not valid a snackbar will be displayed with appropriate message and if the user just hit enter a snackbar will also be displayed with appropriate message. AppNotFound is the component which wraps snackbar.
I'm using redux thunks to dispatch an action which checks whether the app id is valid (it's an async function) inside onKeyDown method (getAppInfo). Ideally I'd like to get the result from the redux already in onKeyDown method. But because the action is dispatched asynchronically I can't.
So I thought to let render display a snackbar based on the value of found property (whether app was found or not). So at first found would be undefined because the async dispatch wouldn't have returned the result in the render but then found would become true or false and then we can display the snackbar. The render would automatically be called the second time because the props have changed. But this doesn't happen.
What is the correct way in terms of patterns to achieve what I want? I don't want to use componentWillReceiveProps as it's deprecated.
This is my code:
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import InputBase from '#material-ui/core/InputBase';
import { withStyles } from '#material-ui/core/styles';
import { withRouter } from 'react-router-dom';
import { connect } from 'react-redux';
import { getAppInfo } from '../../actions/appActions.js';
import constants from '../../constants.js';
import { AppSearchBarInputStyles } from '../styles/Material-UI/muiStyles.js';
import AppNotFound from './AppNotFound.js';
import * as log from 'loglevel';
log.setLevel("debug")
class AppSearchBarInput extends Component {
state = {
appId: '',
snackBarOpen: false
}
onChange = e => {
this.setState({ appId: e.target.value });
}
onKeyDown = e => {
const { appId } = this.state;
const { found } = this.props.app;
if (e.keyCode === constants.ENTER_KEY) {
this.props.getAppInfo({ appId });
if (found) {
this.props.history.push('/moreInfo');
} else {
this.setState({
snackBarOpen: true
});
}
this.setState({
appId: ''
});
}
}
handleCloseSnackBar = () => {
this.setState({
snackBarOpen: false
});
}
render() {
const { classes, app } = this.props;
const { snackBarOpen } = this.state;
const { found, appId } = app;
let message = '';
if (!found) {
message = appId === '' ? constants.MESSAGES.APP_BLANK() : constants.MESSAGES.APP_NOT_FOUND(appId);
}
let displayWhenFoundIsUndefined = null;
if (found === undefined) {
displayWhenFoundIsUndefined = <div>Loading...</div>;
} else {
displayWhenFoundIsUndefined = <AppNotFound message={message}
open={!found}
onClose={this.handleCloseSnackBar}/>;
}
return (
<div>
<InputBase
placeholder="Search…"
classes={{
root: classes.inputRoot,
input: classes.inputInput,
}}
onChange={this.onChange}
onKeyDown={this.onKeyDown}
value={this.state.appId} />
{displayWhenFoundIsUndefined}
</div>
)
}
}
AppSearchBarInput.propTypes = {
classes: PropTypes.object.isRequired
}
const mapStateToProps = state => ({
app: state.app.app
});
const AppSearchBarWithStyles = withStyles(AppSearchBarInputStyles)(AppSearchBarInput);
const AppSearchBarWithStylesConnected = connect(mapStateToProps, { getAppInfo })(AppSearchBarWithStyles);
export default withRouter(AppSearchBarWithStylesConnected);
Since no one still didn't answer this question, I will offer the solution which I recently arrived to.
The problem essentially is that the display of AppSearchBarInput depends mainly on whether the app was found or not. This action must be asynchronic because the information is from Web API. Therefore, I was using redux-thunks and was receiving mobile app information in props. However the snackbarOpen property was in the state which is a problem because the snackbar depends on state property which in itself depends on props which are received asynchronically.
The solution to the predicament is to move snackbarOpen to props as well. So now an action to set snackbarOpen to true should be dispatched directly from getAppInfo thunk if the app was not found, as well as from onKeyDown when the app is blank. On the other hand an action to set snackbarOpen to false should be dispatched from handleCloseSnackBar.
Say I have two redux connected components. The first is a simple todo loading/display container, with the following functions passed to connect(); mapStateToProps reads the todos from the redux state, and mapDispatchToProps is used to request the state to be provided the latest list of todos from the server:
TodoWidgetContainer.js
import TodoWidgetDisplayComponent from '...'
function mapStateToProps(state) {
return {
todos: todoSelectors.getTodos(state)
};
}
function mapDispatchToProps(dispatch) {
return {
refreshTodos: () => dispatch(todoActions.refreshTodos())
};
}
connect(mapStateToProps, mapDispatchTo)(TodoWidgetDisplayComponent);
The second redux component is intended to be applied to any component on a page so that component can indicate whether a global "loading" icon is displayed. Since this can be used anywhere, I created a helper function that wraps MapDispatchToProps in a closure and generates an ID for each component, which is used to make sure all components that requested the loader indicate that they don't need it anymore, and the global loader can be hidden.
The functions are basically as follows, with mapStateToProps exposing the loader visibility to the components, and mapDispatchToProps allowing them to request the loader to show or hide.
Loadify.js
function mapStateToProps(state) {
return {
openLoader: loaderSelectors.getLoaderState(state)
};
}
function mapDispatchToProps() {
const uniqId = v4();
return function(dispatch) {
return {
showLoader: () => {
dispatch(loaderActions.showLoader(uniqId));
},
hideLoader: () => {
dispatch(loaderActions.hideLoader(uniqId));
}
};
};
}
export default function Loadify(component) {
return connect(mapStateToProps, mapDispatchToProps())(component);
}
So now, if I have a component that I want to give access to the loader, I can just do something like this:
import Loadify from '...'
class DisplayComponent = new React.Component { ... }
export default Loadify(DisplayComponent);
And it should give it a unique ID, allow it to request the loader to show/hide, and as long as there is one component that is requesting it to show, the loader icon will show. So far, this all appears to be working fine.
My question is, if I would like to apply this to the todos component, so that that component can request/receive its todos while also being allowed to request the loader to show while it is processing, could I just do something like:
TodoWidgetContainer.js
import Loadify from '...'
import TodoWidgetDisplayComponent from '...'
function mapStateToProps(state) {
return {
todos: todoSelectors.getTodos(state)
};
}
function mapDispatchToProps(dispatch) {
return {
refreshTodos: () => dispatch(todoActions.refreshTodos())
};
}
const TodoContainer = connect(mapStateToProps, mapDispatchTo)(TodoWidgetDisplayComponent);
export default Loadify(TodoContainer);
And will redux automatically merge the objects together to make them compatible, assuming there are no duplicate keys? Or will it take only the most recent set of mapStateToProps/mapDispatchTo unless I do some sort of manual merging? Or is there a better way to get this kind of re-usability that I'm not seeing? I'd really rather avoid having to create a custom set of containers for every component we need.
connect will automatically merge together the combination of "props passed to the wrapper component", "props from this component's mapState", and "props from this component's mapDispatch". The default implementation of that logic is simply:
export function defaultMergeProps(stateProps, dispatchProps, ownProps) {
return { ...ownProps, ...stateProps, ...dispatchProps }
}
So, if you stack multiple levels of connect around each other , the wrapped component will receive all of those props as long as they don't have the same name. If any of those props do have the same name, then only one of them would show up, based on this logic.
Alright, here is what I would do. Create a higher order component (HOC) that adds a new spinner reference to your reducer. The HOC will initialize and destroy references to the spinner in redux by tying into the life cycle methods. The HOC will provide two properties to the base component. The first is isLoading which is a function that takes a boolean parameter; true is on, false is off. The second property is spinnerState that is a readonly boolean of the current state of the spinner.
I created this example without the action creators or reducers, let me know if you need an example of them.
loadify.jsx
/*---------- Vendor Imports ----------*/
import React from 'react';
import { connect } from 'react-redux';
import v4 from 'uuid/v4';
/*---------- Action Creators ----------*/
import {
initNewSpinner,
unloadSpinner,
toggleSpinnerState,
} from '#/wherever/your/actions/are'
const loadify = (Component) => {
class Loadify extends React.Component {
constructor(props) {
super(props);
this.uniqueId = v4();
props.initNewSpinner(this.uniqueId);;
this.isLoading = this.isLoading.bind(this);
}
componentWillMount() {
this.props.unloadSpinner(this.uniqueId);
}
// true is loading, false is not loading
isLoading(isOnBoolean) {
this.props.toggleSpinner(this.uniqueId, isOnBoolean);
}
render() {
// spinners is an object with the uuid as it's key
// the value to the key is weather or not the spinner is on.
const { spinners } = this.props;
const spinnerState = spinners[this.uniqueId];
return (
<Component isLoading={this.isLoading} spinnerState={spinnerState} />
);
}
}
const mapStateTopProps = state => ({
spinners: state.ui.spinners,
});
const mapDispatchToProps = dispatch => ({
initNewSpinner: uuid => dispatch(initNewSpinner(uuid)),
unloadSpinner: uuid => dispatch(unloadSpinner(uuid)),
toggleSpinner: (uuid, isOn) => dispatch(toggleSpinnerState(uuid, isOn))
})
return connect(mapStateTopProps, mapDispatchToProps)(Loadify);
};
export default loadify;
Use Case Example
import loadify from '#/location/loadify';
import Spinner from '#/location/SpinnerComponent';
class Todo extends Component {
componentWillMount() {
this.props.isLoading(true);
asyncCall.then(response => {
// process response
this.props.isLoading(false);
})
}
render() {
const { spinnerState } = this.props;
return (
<div>
<h1>Spinner Testing Component</h1>
{ spinnerState && <Spinner /> }
</div>
);
}
}
// Use whatever state you need
const mapStateToProps = state => ({
whatever: state.whatever.youneed,
});
// use whatever dispatch you need
const mapDispatchToProps = dispatch => ({
doAthing: () => dispatch(doAthing()),
});
// Export enhanced Todo Component
export default loadify(connect(mapStateToProps, mapDispatchToProps)(Todo));
I am new to Redux and i've been having a hard time rendering changes made to the store. I've been using Redux-DevTools to explore state changes and here is my problem.
I have a sidebar which has expanded state as true/false. Initial state is below.
{expanded: false}
During the toggle action i trigger store.dispatch to change the state to true.
In React-DevTools i could see that my state is being changed, the console also logs the change when executed from within my sidenav component.
I have a home page which is able to fetch the initial state of the store, however the actions from sidenav which updates the store doesn't re-render(props val dint change) the home page.
I strongly feel the issue is related to this SO post but not able to get around the wrapped component concept. Any advice.
React Redux - changes aren't reflected in component
Below is code.
Redux Store
const rootReducer = combineReducers({
sideBarReducerMain: sidebarReducer })
export const configurestore = () => {
const store = createStore(rootReducer, composeWithDevTools(
));
return store; }
Action generators
export const onExpand = {
type: 'EXPAND',
payload: {
expanded: true
}
}
export const onCollapse = {
type: 'COLLAPSE',
payload: {
expanded: false
}
}
Reducer
export const sidebarReducer = (state = {expanded:false},action) => {
switch (action.type) {
case 'EXPAND':
return Object.assign({}, state, {expanded: true})
case 'COLLAPSE':
return Object.assign({}, state, {expanded: false})
default:
return state;
}
}
SideBar Toggle (The console logs the new state on every toggle)
onToggle = (expanded) => {
expanded ? store.dispatch(onExpand):store.dispatch(onCollapse)
this.setState({ expanded: expanded });
//outputs the state change looks fine.
console.log("State of store :: " + store.getState());
};
Home page
import React from 'react';
import styled from 'styled-components'
import Main from './MainAlign'
import { connect } from 'react-redux'
class HomePage extends React.Component {
getExpansionState() {
console.log("From render" + this.props.expandedState)
return this.props.expandedState
}
componentWillReceiveProps(nextProps) {
console.log("Looks like this never gets called :( " + this.props.expandedState)
}
render(props) {
return (
<div>
<Main expanded={this.getExpansionState()}>
<h1>Welcome</h1>
<p>This is my site. Take a look around!</p>
</Main>
</div>
)
}
}
const mapStateToProps = state => {
console.log("New state " + state.expanded)
return {
expandedState: state.expanded
}
}
export default connect(mapStateToProps)(HomePage);
REDUX DevTools
You need to declare action creators (functions) which return actions (objects):
export const onExpand = () => ({
type: "EXPAND",
payload: {
expanded: true
}
});
Then instead of calling store.dispatch, you should connect the action creator:
withRouter(connect(null, { onExpand, onCollapse })(SideBar))
And invoke it in your component:
if (expanded) {
this.props.onExpand();
} else {
this.props.onCollapse();
}
Working code:
Misc
this.setState({ expanded: expanded });
console.log("FROM SIDENAV" + this.state.expanded);
setState is asynchronous so you need use the callback to access the updated value:
this.setState({ expanded }, () => console.log("FROM SIDENAV" + this.state.expanded));
Also, there's no need to replicate the redux state in local component (single source of truth).
You can also combine onExpand and onCollapse into a single onToggle function and perform the toggle in reducer: expanded: !state.expanded, or pass the expanded flag as payload.
Change your mapStateToProps function like that:
const mapStateToProps = state => {
console.log("New state " + state.expanded)
return {
expandedState: state.sideBarReducerMain.expanded
}
}
You have a sideBarReducerMain state in your root,global state. This sideBarRecuerMain state has an expanded value not the global one.
Also, you don't need a payload for your action creators since you don't use them. Just a type is enough for your logic.
I am using this starter kit https://github.com/davezuko/react-redux-starter-kit and am following some tutorials at the same time, but the style of this codebase is slightly more advanced/different than the tutorials I am watching. I am just a little lost with one thing.
HomeView.js - This is just a view that is used in the router, there are higher level components like Root elsewhere I don't think I need to share that, if I do let me know, but it's all in the github link provided above.
import React, { PropTypes } from 'react'
import { connect } from 'react-redux'
import { searchListing } from '../../redux/modules/search'
export class HomeView extends React.Component {
componentDidMount () {
console.log(this.props)
}
render () {
return (
<main onClick={this.props.searchListing}>
<NavBar search={this.props.search} />
<Hero/>
<FilterBar/>
<Listings/>
<Footer/>
</main>
)
}
}
I am using connect() and passing in mapStateToProps to tell the HomeView component about the state. I am also telling it about my searchListing function that is an action which returns a type and payload.
export const searchListing = (value) => {
console.log(value)
return {
type: SEARCH_LISTINGS,
payload: value
}
}
Obviously when I call the method inside the connect() I am passing in an empty object searchListing: () => searchListing({})
const mapStateToProps = (state) => {
return {
search: { city: state.search }
}
}
export default connect((mapStateToProps), { searchListing: () => searchListing({}) })(HomeView)
This is where I am stuck, I am trying to take the pattern from the repo, which they just pass 1, I think anytime that action is created the logic is just add 1 there is no new information passed from the component.
What I am trying to accomplish is input search into a form and from the component pass the users query into the action payload, then the reducer, then update the new state with the query. I hope that is the right idea.
So if in the example the value of 1 is hardcoded and passed into the connect() method, how can I make it so that I am updating value from the component dynamically? Is this even the right thinking?
You almost got it right. Just modify the connect function to pass the action you want to call directly:
const mapStateToProps = (state) => ({
search: { city: state.search }
});
export default connect((mapStateToProps), {
searchListing
})(HomeView);
Then you may use this action with this.props.searchListing(stringToSearch) where stringToSearch is a variable containing the input value.
Notice : You don't seem to currently retrieve the user query. You may need to retrieve it first and then pass it to the searchListing action.
If you need to call a function method, use dispatch.
import { searchListing } from '../../redux/modules/search';
const mapDispatchToProps = (dispatch) => ({
searchListing: () => {
dispatch(searchListing());
}
});
export default connect(mapStateToProps, mapDispatchToProps)(HomeView);
Then, you have made the function a prop, use it with searchListing.