component not re-renders as expected when redux store updates - javascript

in my react-redux app one of the components is rendering "box" components according to a number passed down as props through the store. the number is controlled by a slider controlled component, that for sure changes the store as i can see with my redux dev tools.
at first render the component renders the boxes as expected inside their wrapper div, but as soon as i move the slider and change the number all i get is a single box component.
i've tried to change the component into a stateful one and use different hooks but so far without success.
here is the component:
import React from 'react';
import { connect } from 'react-redux';
import Box from './Box';
const Grid = ({ boxNumber }) => {
return(
<div className='flex flex-wrap'>
{new Array(boxNumber).fill(null).map((box, i) => <Box key={i} />)}
</div>
)
}
const mapStateToProps = (state) => ({
boxNumber: state.boxNumberReducer.boxNumber
})
export default connect(mapStateToProps)(Grid);
i'm adding here the reducer and action just in case even though i don't believe that's where the issue is, but maybe i'm missing something.
reducer:
import { SET_BOX_NUMBER } from '../actions/constants';
const initialState = {
boxNumber: 100
}
export default (state = initialState , {type, payload}) => {
switch (type) {
case SET_BOX_NUMBER:
return {...state, boxNumber: payload};
default:
return state;
}
}
action:
export const setBoxNumber = (payload) => ({
type: SET_BOX_NUMBER, payload
})
here is the box component, i'm using tailwindcss so it's basically a div with height and width of 2rem, a border and a white background color:
import React from 'react';
const Box = () => {
return(
<div className='w-8 h-8 border border-black bg-white'>
</div>
)
}
export default Box;
EDIT:
this is the slider component where the action is being dispatched:
import React from 'react';
import { connect } from 'react-redux';
import { setBoxNumber } from '../actions';
const Slider = ({ boxNumber, handleChange }) => {
return(
<div className='slider p-1 m-1'>
<div className='flex justify-center'>
{boxNumber}
</div>
<div className='flex justify-center'>
<input
onChange={handleChange}
value={boxNumber}
type="range"
step='10'
min="10"
max="500"
/>
</div>
</div>
)
}
const mapStateToProps = (state) => ({
boxNumber: state.boxNumberReducer.boxNumber
});
const mapDispatchToProps = {
handleChange: (event) => setBoxNumber(event.target.value)
}
export default connect(mapStateToProps, mapDispatchToProps)(Slider);

You need to convert event.target.value to Number in your Slider component, because you are passing the value as string to new Array(boxNumber)
const mapDispatchToProps = {
handleChange: (event) => setBoxNumber(Number(event.target.value))
}

Related

redux state gets -improperly- updated before reducers is called (w/ ReactDnD)

Edit: the bug was is a separated helper function that was mutating the state (not displayed in the post).
I'm experimenting with ReactDnD to create a sortable image grid via drag and drop. I've been following this tutorial 1 and trying to implement it with redux instead of React Context.
The issue that I'm having is that my props don't get updated after I re-arrange the images. I have been debugging the reducers and noticed that the state gets somehow updated before the reducer has the chance to do so (which would trigger mapStateToProps to reload my component with the updated state). The problem though it that I have no idea why that happens. I have the feeling that since ReactDnD is also using Redux, it's somehow causing this.
Here are the different parts:
Index.js
export const store = createStore(reducers, applyMiddleware(thunk))
ReactDOM.render(
<Provider store={store}>
<DndProvider backend={HTML5Backend}>
<App />
</DndProvider>
</Provider>,
document.getElementById('root')
)
App.js (parent component of DroppableCell and DraggableItem)
class App extends React.Component {
componentDidMount() {
this.props.loadCollection(imageArray)
}
render() {
return (
<div className='App'>
<div className='grid'>
{this.props.items.map((item) => (
<DroppableCell
key={item.id}
id={item.id}
onMouseDrop={this.props.moveItem}
>
<DraggableItem src={item.src} alt={item.name} id={item.id} />
</DroppableCell>
))}
</div>
</div>
)
}
}
const mapStateToProps = (state) => {
return { items: state.items }
}
export default connect(mapStateToProps, {
moveItem,
loadCollection,
})(App)
DroppableCell (calling the action creator from parent component)
import React from 'react'
import { useDrop } from 'react-dnd'
const DroppableCell = (props) => {
const [, drop] = useDrop({
accept: 'IMG',
drop: (hoveredOverItem) => {
console.log(hoveredOverItem)
props.onMouseDrop(hoveredOverItem.id, props.id)
},
})
return <div ref={drop}>{props.children}</div>
}
export default DroppableCell
DraggableItem
import React from 'react'
import { useDrag } from 'react-dnd'
const DraggableItem = (props) => {
const [, drag] = useDrag({
item: { id: props.id, type: 'IMG' },
})
return (
<div className='image-container' ref={drag}>
<img src={props.src} alt={props.name} />
</div>
)
}
export default DraggableItem
Reducer
import { combineReducers } from 'redux'
const collectionReducer = (state = [], action) => {
// state is already updated before the reducer has been run
console.log('state:', state, 'action: ', action)
switch (action.type) {
case 'LOAD_ITEMS':
return action.payload
case 'MOVE_ITEM':
return action.payload
default:
return state
}
}
export default combineReducers({
items: collectionReducer,
})
The action creator
export const moveItem = (sourceId, destinationId) => (dispatch, getState) => {
const itemArray = getState().items
const sourceIndex = itemArray.findIndex((item) => item.id === sourceId)
const destinationIndex = itemArray.findIndex(
(item) => item.id === destinationId
)
const offset = destinationIndex - sourceIndex
//rearrange the array
const newItems = moveElement(itemArray, sourceIndex, offset)
dispatch({ type: 'MOVE_ITEM', payload: newItems })
}
found the bug - unfortunately was outside the code posted as I thought it was a simple helper function. I realised I was using the 'splice' method to rearrange the imageArray, and therefore mutating the state.

Does state persist upon rerender of components?

I wanna know why will my state remain the same upon rerender of the components.
Here is my parent component
import React, {useEffect} from 'react';
import {connect} from "react-redux"
import NoteList from '../Components/NoteList';
import NoteDetail from '../Components/NoteDetail';
import "./NotePage.scss"
const NotePage = ({note}) => {
const selectNote = (item) => {
if (!item){
return <div className="emptySection"/>
}
console.log("return a new component")
return <NoteDetail/>
}
return (
<div className="NotePage">
<div className="noteList">
<NoteList/>
</div>
<div className="noteDetail">
{selectNote(note)}
</div>
</div>
)
}
const mapState = (state) => (
{
note: state.notesReducer.note
}
)
export default connect(mapState)(NotePage);
I have already checked that this component rerender when the note in redux store changed. The selectNote is executed proper as well. But does it return a brand new component of NoteDetail?
Here is my child component:
import React, {useState, useRef, useEffect} from 'react';
import {connect} from "react-redux";
import Editor from 'draft-js-plugins-editor';
import {EditorState, ContentState} from "draft-js";
import CreateInlineToolbarPlugin from "draft-js-inline-toolbar-plugin";
import CustomInlineToolbar from "./CustomInlineToolbar";
import {updateNote} from "../redux/action"
import "./NoteDetail.scss";
import 'draft-js-inline-toolbar-plugin/lib/plugin.css';
const InlineToolbarPlugin = CreateInlineToolbarPlugin();
const {InlineToolbar} = InlineToolbarPlugin;
const NoteDetail = ({updateNote, title, note, date, _id}) => {
let initContentState = ContentState.createFromText(note);
let [editorState, setEditorState] = useState(EditorState.createWithContent(initContentState));
let [titleState, setTitleState] = useState(title)
let editorRef = useRef(React.createRef());
useEffect(()=>{
updateNote(_id, titleState, editorState)
},[editorState, titleState, _id])
const focus = () => {
editorRef.current.focus()
}
return(
<div>
<div className="NoteDetail-container">
<input type="text" id="title"
value={titleState === "Untitled Page"? "":titleState}
onChange={(e)=>setTitleState(e.target.value)}
/>
<div id="content" onClick={focus}>
<Editor
plugins={[InlineToolbarPlugin]}
onChange={(e)=>setEditorState(e)}
editorState={editorState}
ref={ele => editorRef.current = ele}
/>
<CustomInlineToolbar InlineToolbar={InlineToolbar} />
</div>
</div>
</div>
)
};
const mapProps = (state) => {
const {title, _id, note, date} = state.notesReducer.note
return {
title,
_id,
note,
date
}
}
export default connect (mapProps, {updateNote})(NoteDetail)
It rerenders when the note state changed, but the state remain the same. I checked that the title did change, but the titleState didn't. So how does state and component comparison behave in React?
my reducer:
import {
GET_NOTE,
UPDATE_NOTE,
DEL_NOTE,
CREATE_NOTE,
SELECT_NOTE,
GET_NOTES,
} from "../action/types";
import {
api_getNote,
api_delNote,
api_updateNote,
api_postNote
} from "../../api/noteService"
//TODOS: finish update_title part
const notesReducer = ( state={}, action) => {
switch (action.type){
case GET_NOTES:
return{notes: action.payload}
case UPDATE_NOTE:
const {id, content} = action.payload
api_updateNote(id,content)
return state
case DEL_NOTE:
api_delNote(action.payload)
return state
case CREATE_NOTE:
api_postNote(action.payload, (res)=> {
return {notes: res}
})
return state
case SELECT_NOTE:
return {...state, note: action.payload}
default:
return state
}
}
export default notesReducer

Why is my application state not updating (redux)

I am following the redux counter tutorial from the official docs,- but my applications state is seemingly not updating. To be more clear, essentially the application is a counter with an increment button and a decrement button and it displays the current value on the screen.
I can get it to console log the value as it changes, but it doesn't output it on the screen. Can anyone tell me what I'm doing wrong
import React, { Component } from 'react';
import { createStore } from 'redux';
const counter = (state = 0, action) => {
switch(action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state -1;
default:
return state;
}
}
const store = createStore(counter);
store.subscribe(()=>{
console.log(store.getState());
});
class App extends Component {
render() {
return (
<div className="App">
<h1>Counter Application</h1>
<hr/>
<Counter
value={store.getState()}
onIncrement={() => store.dispatch({type: 'INCREMENT'})}
onDecrement={() => store.dispatch({type: 'DECREMENT'})}
/>
</div>
);
}
}
const Counter = ({
value,
onIncrement,
onDecrement
}) => {
return(
<div>
<h1>{value}</h1>
<button onClick={onIncrement}> Plus </button>
<button onClick={onDecrement}> Minus </button>
</div>
)
}
export default App;
You're gonna need the provider and connect components from react-redux
Your App component won't re-render.
AFAIK, simply because a re-render can only be triggered if a component’s state or props has changed.
You need to trigger the your App component re-render inside store subscribe. I see your store subscribe basically do nothing, only logging here.
store.subscribe(()=>{
console.log(store.getState());
});
you could do something like this, to trigger re-render every time redux store updated:
const page = document.getElementById('page');
const render = () => ReactDOM.render(<App />, page);
render();
store.subscribe(render);
The reason:
In your case, the component has no idea about the changes in the redux store and therefore it doesn't re-render.
Components are only re-rendering if they receiv new props/context
or if their local state has updated (as a result of calling setState() in general)
Solution 1 (direct answer to your question, I think)
const Counter = ({ value, onIncrement, onDecrement }) => {
return (
<div>
<h1>{value}</h1>
<button onClick={onIncrement}> Plus</button>
<button onClick={onDecrement}> Minus</button>
</div>
)
};
class App extends Component {
componentDidMount() {
this._unsub = store.subscribe(this._update);
this._update();
}
componentWillUnmount() {
this._unsub();
this._unsub = null;
};
state = { value: undefined };
render() {
const { value } = this.state;
return (
<div className="App">
<h1>Counter Application</h1>
<Counter
value={value}
onIncrement={this._increment}
onDecrement={this._decrement}
/>
</div>
);
}
_decrement = () => store.dispatch({ type: 'DECREMENT' });
_increment = () => store.dispatch({ type: 'INCREMENT' });
_update = () => {
const value = store.getState();
this.setState({ value });
}
}
Solution 2 (the correct one)
Use react-redux module
Also check these:
- normalizr
- normalizr + keyWindow concept talk
- Reselect
- ComputingDerivedData Post
- react-reselect-and-redux post
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { _valueSelector1, _valueSelector2 } from 'app/feature/selectors';
import { increment, decrement } from 'app/feature/actions';
const mapStateToProps = (state, props) => ({
valueOne: _valueSelector1(state, props),
valueTwo: _valueSelector2(state, props),
})
const mapDispatchToProps = {
increment,
decrement,
};
#connect(mapStateToProps, mapDispatchToProps)
export class YourComponent extends Component {
static propTypes = {
valueOne: PropTypes.number,
valueTwo: PropTypes.number,
increment: PropTypes.func.isRequired,
decrement: PropTypes.func.isRequired,
}
render() {
const { valueOne, valueTwo, increment, decrement } = this.props;
return (
<div>
<span>{valueOne}</span>
<Counter value={valueTwo} onIncrement={increment} onDecrement={decrement} />
</div>
)
}
}

React Redux - Setting Store and Updating UI using Navigation Menu

I'm extremely new to react/redux and very mediocre at Javascript, but I've been struggling with this for over a week now. Here's what I'm trying to do:
Clicking one of the navigation menu items on the left should dispatch an action to set the selected index in the store (at this point the entire store is just a number). When the index is updated it should automatically be reflected in the UI, at least by changing the css class of the selected item, but eventually it should toggle visibility for content components on the right.
Sidebar.js:
import React, { Component } from 'react';
import SidebarItem from './SidebarItem'
import ActionCreators from '../actions'
import { connect } from 'react-redux'
export default class Sidebar extends Component
{
handleClick(index)
{
//Dispatch action here maybe?
this.props.selectedSidebarItem(index);
console.log(this);
}
render()
{
var sidebarItemNames = ["Verify Original Contract", "Evaluate Transfer Terms", "Create Future Customer", "Check Credit", "Generate Agreement", "Finalize Transfer"];
return (
<div>
<div id="sidebar-title">
<div id="sc-logo">
LOGO
</div>
<div>Contract Transfer for:</div>
<div>XYZ</div>
<br />
</div>
<ul className="list-group" id="sidebar-list">
{sidebarItemNames.map(function(n, index)
{
return <SidebarItem key={index} index={index} selectedIndex={this.selectedSidebarItem} name={n} handleClick={this.handleClick(index).bind(this)} />;
})}
</ul>
</div>
)
}
}
function mapDispatchToProps(dispatch) {
return {
selectedSidebarItem: (index) => dispatch(ActionCreators.setSelectedSidebarItem(index))
}
}
const conn = connect(
null,
mapDispatchToProps
)(Sidebar)
SidebarItem.js:
import React, { Component } from 'react';
import ActionCreators from '../actions'
import { connect } from 'react-redux'
export class SidebarItem extends Component {
constructor(props) {
super(props);
}
setSelectedSidebarItem() {
this.props.handleClick(this.props.index);
this.props.selectedSidebarItem(this.props.index);
// const ul = document.getElementById('sidebar-list');
// const items = ul.getElementsByTagName('li');
// for (let i = 0; i < items.length; ++i) {
// items[i].classList.remove('sidebar-item-current');
// }
}
render() {
return (
<li className={"list-group-item sidebar-list-item sidebar-item-todo" + (this.props.index==this.props.selectedIndex? ' sidebar-item-current':'') } onClick={this.setSelectedSidebarItem.bind(this)}><i className="fa fa-circle fa-lg"></i> <span>{this.props.name}</span></li>
)
}
}
Store.js:
import { createStore } from 'redux'
import reducers from './Reducers'
const store = createStore(reducers)
export default store
Reducers.js
const initialState = {
selectedSidebarItem: window.initialPageState,
otherStuff: 5
};
const reducers = (state = initialState, action) => {
switch (action.type) {
case "SET_SELECTED_SIDEBAR_ITEM":
console.log("clicked sidebar index: " + action.index);
var result = Object.assign({}, state, {
selectedSidebarItem: action.index
})
console.log(result);
return result;
default:
return state
}
}
export default reducers
actions.js:
import constants from './constants'
let ActionCreators = {
setSelectedSidebarItem(index) {
var actionObject = {
type: constants.UPDATE_SELECTED_SIDEBAR_ITEM,
index
}
console.log("setting sidebar item", actionObject);
return actionObject
}
}
export default ActionCreators
Constants.js
const constants = {
UPDATE_SELECTED_SIDEBAR_ITEM: "UPDATE_SELECTED_SIDEBAR_ITEM",
ADD_ERROR: "ADD_ERROR",
CLEAR_ERROR: "CLEAR_ERROR"
};
export default constants;
I've tried a few variations of the above and have previously been able to dispatch actions, but am unsure the store is ever updated and nothing is reflected on the UI. Right now I'm getting this error when clicking sidebar items: "Cannot read property 'handleClick' of undefined"
Thanks for any help in advance.
in your sidebar.js:
instead of
handleClick={() => this.handleClick(index).bind(this)}
try this:
handleClick={this.handleClick(index).bind(this)}
And in handleClick method you have to dispatch action:
this.props.selectedSidebarItem(index)
Answer update:
import React, { Component } from 'react';
import SidebarItem from './SidebarItem'
import ActionCreators from '../actions'
import { connect } from 'react-redux'
export default class Sidebar extends Component
{
handleClick(index)
{
//Dispatch action here maybe?
this.props.selectedSidebarItem(index);
this.selectedSidebarItem = index;
console.log(this);
}
render()
{
var sidebarItemNames = ["Verify Original Contract", "Evaluate Transfer Terms", "Create Future Customer", "Check Credit", "Generate Agreement", "Finalize Transfer"];
return (
<div>
<div id="sidebar-title">
<div id="sc-logo">
LOGO
</div>
<div>Contract Transfer for:</div>
<div>XYZ</div>
<br />
</div>
<ul className="list-group" id="sidebar-list">
{sidebarItemNames.map(function(n, index)
{
return <SidebarItem key={index} index={index} selectedIndex={this.selectedSidebarItem} name={n} handleClick={this.handleClick(index).bind(this)} />;
})}
</ul>
</div>
)
}
}
function mapDispatchToProps(dispatch) {
return {
selectedSidebarItem: (index) => dispatch(ActionCreators.setSelectedSidebarItem(index))
}
}
const conn = connect(
null,
mapDispatchToProps
)(Sidebar)

How to pass state to more then one component in redux

I start working with React + Redux and newbie to the framework.*
What is am trying to do?
I have to click a button and on the click it will show an svg path in other container/svg tag.
My code or my code
import { connect } from "react-redux";
import { BodyTemplate, Button } from "../component/svgPreview/Preview.jsx";
import { previewBodyTemplate } from "../modcon/Actions.js";
const mapStateToProps = ({ previewTemplateState }) => {
return ({
previewTemplateState: previewTemplateState
});
};
const mapDispatchToProps = (dispatch) => {
return ({
onImageClick: (value) => {
dispatch(previewBodyTemplate(value));
}
});
};
/* the following code will work because we cannot export more then one by
default **/
/*
const PreviewContainer = connect(mapStateToProps, mapDispatchToProps)(Button);
const PreviewBody = connect(mapStateToProps, mapDispatchToProps)(BodyTemplate);
export default PreviewContainer;
export default PreviewBody;
*/
/* so i try this */
export const PreviewContainer = connect(mapStateToProps, mapDispatchToProps)(Button);
export const PreviewBody = connect(mapStateToProps)(BodyTemplate);
According to my knowlage i am passing the state to two components so when the state of one update it will update the other.
But the file is not working because we and not export it directly.
How i have to tackle to pass the state to more then one components
below is the error when i am exporting directly
*
You need to create a parent component, this component will contain the button and the preview component.
const MyContainer = ({ image, onImageClick }) => (
<div>
<div className="sidebar">
<Button label="Preview image" onClick={onImageClick} />
</div>
<div className="content">
<Preview source={image} />
</div>
</div>
);
Once you have your container ready, you need to map the props of this component to the state/actions from redux.
const mapStateToProps = ({ image, previewTemplateState }) => {
return ({
image: image,
previewTemplateState: previewTemplateState
});
};
const mapDispatchToProps = (dispatch) => {
return ({
onImageClick: (value) => {
dispatch(previewBodyTemplate(value));
}
});
};
export default connect(mapStateToProps, mapDispatchToProps)(MyContainer);
Now the main container will receive the data and the actions, from there you can send whatever you need to the children components.
The button and the preview image component are stateless component, they just receive props from the parent container.

Categories