It is my first react-redux project (list of courses) and I have some troubles with redux. I need to do the search by course name, I based my code on this answer I can see the action in redux-devtool, but not on ui. Please help me to understand what I am doing wrong
//action
export function search (value) {
return {type: SEARCH, value};
}
//reducer
import { COURSES } from '../../constants';
const initialState = COURSES;
export default function reducer(state = initialState, action) {
switch(action.type) {
case SEARCH: {
const {value} = action;
const course = state.filter((val) => val.includes(value));
return {...state, value, course};
}
default:
return state;
}
}
//search component
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {deleteCourse, search} from '../../redux/actions';
class SearchInput extends Component {
render() {
const {search, value} = this.props;
return (
<div className={classes.SearchInput}>
<input
placeholder='Search'
onChange={(e) => search(e.target.value)}
value={value}
/>
</div>
)
}
}
export default connect (state => ({
courses: state.courses,
}), {search})(SearchInput);
//Courses Component
import Form from '../Form/Form';
import { deleteCourse, search } from '../../redux/actions';
const Courses = ({ courses, deleteCourse }) => {
console.log(courses)
return (
<div className={classes.Courses}>
<SearchInput />
<Button title='Add course' />
<CourseList
courses={courses}
deleteCourse={deleteCourse}
search={search}
/>
<Form />
</div>
)
}
export default connect (state => ({
courses: state.courses,
}), {deleteCourse})(Courses);
image-from-redux-devtool
There's a few things that may be causing it
export function search (value) {
return {type: SEARCH, value};
}
Unless you didn't add your entire action code above (which judging by the dev tool photo may be the case) you're passing an undefined variable as type
import { COURSES } from '../../constants';
const initialState = COURSES;
export default function reducer(state = initialState, action) {
switch(action.type) {
case SEARCH: {
const {value} = action;
const course = state.filter((val) => val.includes(value));
return {...state, value, course};
}
default:
return state;
}
}
In your reducer it looks like your initial state is an array (since you're able to filter it directly), but then you iterate that array as properties of a return object.
const initialState = {courses: [], value: '', matches: []}
const matches = state.courses.filter((val) => val.includes(value));
return {...state, matches: matches};
Your initial state structure shouldn't be restructured on a return; instead it should look like the above, with matches filtered from state.courses. Storing search value in state also doesn't appear to be of any use, so it can be removed.
Overall, I don't think it's necessary to do search through redux as the parent component can manage its own rendering. You just need to create a state property to filter courses passed in by redux connect.
See working example:
https://codesandbox.io/s/zen-boyd-9cgfo?file=/src/App.js
Related
I need to develop a global search filter to search the products based on the product title.
To solve the problem I'm using redux to manage the global formal state. I created a reducer to filter the titles, however, I don't know how to get the array of API objects and play in the initialState data: []; To call the API I'm using createAPI from the redux toolkit.
Some part of code:
import { createSlice } from "#reduxjs/toolkit";
const searchSlice = createSlice({
name: "search",
initialState: {
data: [], // array of objects from API
filteredTitle: [],
isLoading: true
},
reducers: {
getData: (state, action) => {
state.data = action.payload;
},
searchByName: (state, action) => {
const filteredResult = state.data.filter((product) =>
product.title.toLowerCase().includes(action.payload.toLowerCase())
);
return {
...state,
filteredTitle:
action.payload.length > 0 ? filteredResult : [...state.data]
};
}
}
});
export const { searchByName, getData } = searchSlice.actions;
export default searchSlice.reducer;
In App.js
import React, { useState, useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { searchByName, getData } from "./features/slice";
import { useGetAllProductsQuery } from "./services/api";
export default function App() {
const dispatch = useDispatch();
const filteredTitle = useSelector((state) => state.search.filteredTitle);
const [searchTerm, setSearch] = useState("");
const { data } = useGetAllProductsQuery();
console.log("data", data);
const changeSearchTerm = (e) => {
setSearch(e.target.value);
};
useEffect(() => {
dispatch(searchByName(searchTerm));
dispatch(getData(data));
}, [searchTerm, dispatch]);
return (
<div>
<input onChange={changeSearchTerm} type="text" value={searchTerm} />
<div>
{filteredTitle.map((user) => (
<div>{user.name}</div>
))}
</div>
</div>
);
}
CodeSandbox
Redux Toolkit Query is made, so you won't interact with Redux store with thunk functions directly (that's what they say in their documentation).
You can only use transformResponse once creating the query. I haven't come across any ways, to be able to add an extra reducer to the slices that are made with RTK Query.
The first way, is to store the data that you are getting from the query, inside another slice, and then use a reducer to filter your data.
The second way is to create a slice, and using createAsyncThunk, fetch your data, then treat your data as you want it. I can leave an example for you:
export const getName = createAsyncThunk('getNameFromBE',
async name => {
await fetch('https://example.com/studentID)
}
)
export const studnetSlice = createSlice({
name: 'studentSlice',
initialState,
reducers: {
aReducer : (state, action) => {state.name = action.payload}
},
extraReducers: {
[getName .pending]: state => {
state.loading = true
state.failed = false
},
[getName .fulfilled]: (state, action) => {
state.name= action.payload
state.loading = false
},
[getName .rejected]: state => {
state.loading = false
state.rejected = true
},
}
})
Here you have a slice, with thunk and normal reducers, with a state that is accessible, but the downside is that you have to manage fetch completely manually (can be an advantage too), and also the call state (pending, fulfilled, rejected) must be done manually, which takes a lot of boilerplate code writing.
I'm learning React-Redux, and I'm trying to build an app in which I write an input called trial in LIST CREATION, insert it in list and through the function updateList update the initial state of list in the REDUCER, and then display that list in the TRIALLIST screen, since nothing really happened when I launched the code, I've put a console.log in the INDEX.JS and it returns the error Actions must be plain objects. Instead, the actual type was: 'undefined'. You may need to add middleware to your store setup to handle dispatching other values, such as 'redux-thunk' to handle dispatching functions., I've tried googling it but I could not seem to find a solution. Thank you in advance for your help.
LIST CREATION
import { connect } from 'react-redux';
import { updateList } from '../../../../../redux/actions/index.js';
class trial extends Component {
constructor(props) {
super(props);
this.state = {
trial: '',
list: [],
};
}
submitTrial(){
let list = this.state.list;
list.push(this.state.trial);
this.props.updateList(list);
this.props.navigation.navigate("TrialList");
}
render() {
return (
<Button transparent>
<Icon
name="checkmark"
onPress={() => this.submitTrial()}
/>
</Button>
<TextInput
placeholder='type here'
onChangeText={(trial) => this.setState({ trial })}
/>
const mapDispatchToProps = { updateList };
export default connect( mapDispatchToProps )( trial );
TRIALLIST
class TrialList extends Component {
constructor(props) {
super(props);
this.state = {
list: this.props.list,
};
}
render() {
return (
<FlatList
data={this.state.list}
renderItem={({ item }) => (
//some data ///
/>
function mapStateToProps(store){
return{
list: store.userState.list
};
}
export default connect(mapStateToProps)(TrialList);
INDEX.JS
import { ADD_LIST } from "../constants/index";
export const updateList = (list) => {
return console.log({ type: ADD_LIST, payload: list}) <----------------------HERE
}
REDUCER
import { USER_STATE_CHANGE, ADD_LIST } from "../constants";
const initialState = {
currentUser: null,
list: [],
};
export const user = (state = initialState, action) => {
switch (action.type){
case USER_STATE_CHANGE:
return {
...state,
currentUser: action.currentUser,
};
case ADD_LIST:
return{
...state,
list: [...action.payload],
}
default:
return state
}
};
APP.JS
const store = createStore(rootReducer, applyMiddleware(thunk));
export class App extends React.Component {
render() {
return (
<Provider store={store}>
<TrialList/>
<trial/>
<Provide/>
Why are you returning console.log here?
export const updateList = (list) => {
return console.log({ type: ADD_LIST, payload: list}) <----------------------HERE
}
console.log returns undefined, and, if you want to log the action triggered, then, console.log it first and then return the object like this.
export const updateList = (list) => {
console.log({ type: ADD_LIST, payload: list}) <----------------------HERE
return { type: ADD_LIST, payload: list}
}
export const updateList = (list) => ({
type: ADD_LIST, payload: list
})
You are missing the parenthesis around after the arrow and before closing the function body.
Context
The goal is to have a component with a key name being react-rendered in App.js when I press a specific key, registered in another component. The information is being passed thorugh a redux managed state.
The problem
It's simple :
I'm updating my state in my redux reducer but even when duplicating it (I can see it thanks to the redux dev tool that allows me to watch my prevState and my nextState being different)
And the question is as simple :
Why my App.js component won't re-render even after connecting to and
duplicating my state ?
I think I made sure that my state was duplicated with the spreading operation and my redux dev tool display me a good state update without having my prevState and nextState duplicated. I looked through a lot of posts and found only people that forgot to duplicate their state in their reducers, which I did not.
So what's the problem here ??
DevTool Sample
Code
Here is the code, quite simple. The interesting piece is playSound and playedKeys:
App.js :
import React from 'react'
import './App.css';
import { connect } from 'react-redux';
import KeyComponent from './Components/Key'
import SoundPlayer from './Components/Sounds'
const mapStateToProps = (state) => ({
...state.soundReducer
})
class App extends React.Component {
constructor(props) {
super(props);
}
render(){
return (
<div>
{console.log(this.props)}
{
this.props.playedKeys.map(key =>{
<KeyComponent keyCode={key}> </KeyComponent>
})
}
<SoundPlayer></SoundPlayer>
</div>
);
}
}
export default connect(mapStateToProps)(App);
Reducer
export default (state = {allSounds:{},playedKeys:[]}, action) => {
switch (action.type) {
case 'ADD_SOUND':
return reduce_addSound({...state},action)
case 'PLAY_SOUND':
return reduce_playSound({...state,playedKeys : [...state.playedKeys]},action)
default:
return state
}
}
function reduce_addSound (state,action){
let i = 0
state.allSounds[action.payload.key] = { players : new Array(5).fill('').map(()=>(new Audio())) , reader : new FileReader()}
//load audioFile in audio player
state.allSounds[action.payload.key].reader.onload = function(e) {
state.allSounds[action.payload.key].players.forEach(player =>{
player.setAttribute('src', e.target.result);
player.load();
player.id = 'test'+e.target.result+ i++
})
}
state.allSounds[action.payload.key].reader.readAsDataURL(action.payload.input.files[0]);
return state
}
function reduce_playSound(state,action){
state.playedKey = action.payload.key;
if(!state.playedKeys.includes(state.playedKey))
state.playedKeys.push(action.payload.key);
return state
}
Action
export const addSound = (key, input,player) => (dispatch,getState) => {
dispatch({
type: 'ADD_SOUND',
payload: {key : key, input : input}
})
}
export const playSound = (key) => (dispatch,getState) => {
dispatch({
type: 'PLAY_SOUND',
payload: {key : key}
})
}
The component registering the keypresses
import React from 'react'
import { connect } from 'react-redux';
import { playSound } from '../../Actions/soundActions';
const mapStateToProps = (state) => ({
...state.soundReducer
})
const mapDispatchToProps = dispatch => ({
playSound: (keyCode) => dispatch(playSound(keyCode))
})
class SoundPlayer extends React.Component {
constructor(props) {
super(props);
}
componentDidMount () {
this.playSoundComponent = this.playSoundComponent.bind(this)
document.body.addEventListener('keypress', this.playSoundComponent);
}
keyCodePlayingIndex = {};
playSoundComponent(key){
if(this.props.allSounds.hasOwnProperty(key.code)){
if(!this.keyCodePlayingIndex.hasOwnProperty(key.code))
this.keyCodePlayingIndex[key.code] = 0
this.props.allSounds[key.code].players[this.keyCodePlayingIndex[key.code]].play()
this.keyCodePlayingIndex[key.code] = this.keyCodePlayingIndex[key.code] + 1 >= this.props.allSounds[key.code].players.length ? 0 : this.keyCodePlayingIndex[key.code] + 1
console.log(this.keyCodePlayingIndex[key.code])
}
this.props.playSound(key.code);
}
render(){
return <div>
<h1 >Played : {this.props.playedKey}</h1>
{Object.keys(this.keyCodePlayingIndex).map(key =>{
return <p>{key} : {this.keyCodePlayingIndex[key]}</p>
})}
</div>
}
}
export default connect(mapStateToProps, mapDispatchToProps)(SoundPlayer);
Issue
You are mutating your state object.
state.allSounds[action.payload.key] = ...
state.playedKey = action.payload.key;
Solution
Update your reducer functions to return new state objects, remembering to correctly shallow copy each level of depth that is being updated.
export default (state = { allSounds: {}, playedKeys: [] }, action) => {
switch (action.type) {
case 'ADD_SOUND':
return reduce_addSound({ ...state },action);
case 'PLAY_SOUND':
return reduce_playSound({ ...state, playedKeys: [...state.playedKeys] }, action);
default:
return state
}
}
function reduce_addSound (state, action) {
const newState = {
...state, // shallow copy existing state
allSounds: {
...state.allSounds, // shallow copy existing allSounds
[action.payload.key]: {
players: new Array(5).fill('').map(()=>(new Audio())),
reader: new FileReader(),
},
}
};
// load audioFile in audio player
newState.allSounds[action.payload.key].reader.onload = function(e) {
newState.allSounds[action.payload.key].players.forEach((player, i) => {
player.setAttribute('src', e.target.result);
player.load();
player.id = 'test' + e.target.result + i // <-- use index from forEach loop
})
}
newState.allSounds[action.payload.key]
.reader
.readAsDataURL(action.payload.input.files[0]);
return newState;
}
function reduce_playSound (state, action) {
const newState = {
...state,
playedKey: action.payload.key,
};
if(!newState.playedKeys.includes(newState.playedKey))
newState.playedKeys = [...newState.playedKeys, action.payload.key];
return newState
}
Okay I've got it, it's always the simplest stupidest thing that we don't check huh.
Clarification
So my state was properly duplicated with reduce_addSound({ ...state },action) and reduce_playSound({ ...state, playedKeys: [...state.playedKeys] and like I wrote in my question, that wasn't the issue !
Issue
As old as it can get, I wasn't returning a component in my render function.. :
in App.js :
render(){
return (
<div>
{
this.props.soundReducer.playedKeys.map(key =>{
<KeyComponent keyCode={key}> </KeyComponent> //<-- NO return or parenthesis !!
})
}
<SoundPlayer></SoundPlayer>
</div>
);
}
Answer
App.js render function with parenthesis:
render(){
return (
<div>
{
this.props.soundReducer.playedKeys.map(key =>(
<KeyComponent key = {key} keyCode={key}> </KeyComponent> //<-- Here a component is returned..
))
}
<SoundPlayer></SoundPlayer>
</div>
);
}
So, i wrote a test project to explore react, react-router and react-redux.
After i got everything working fine i laid my eyes again on Settings.jsx and i am wondering how could i make it less verbose and error prone:
import React, { Component } from "react";
import { connect } from "react-redux";
class Settings extends Component {
state = { name: this.props.settings.name };
render() {
return (
<div>
<h1>Settings</h1>
<p>This is Settings page</p>
My name is{" "}
<input
value={this.state.name}
onChange={e => this.setState({ name: e.target.value })}/>
<button onClick={e => this.props.changeName(this.state.name)}>
Change
</button>
</div>
);
}
}
const mapState = state => ({ settings: state.settings });
const mapDispatch = dispatch => {
return {
changeName(name) {
dispatch({ type: "setName", name });
}
};
};
export default connect(
mapState,
mapDispatch
)(Settings);
My first idea was to convert it into a functional component, but it's said that they don't have state and i need the state to locally handle the input.
With #babel/plugin-proposal-decorators, connect can be used as a decorator:
import React, { Component } from "react";
import { connect } from "react-redux";
const mapState = state => ({ settings: state.settings });
const mapDispatch = dispatch => {
return {
changeName(name) {
dispatch({ type: "setName", name });
}
};
};
#connect(mapState, mapDispatch)
export default class Settings extends Component {
state = { name: this.props.settings.name };
render() {
return (
<div>
<h1>Settings</h1>
<p>This is Settings page</p>
My name is{" "}
<input
value={this.state.name}
onChange={e => this.setState({ name: e.target.value })}/>
<button onClick={e => this.props.changeName(this.state.name)}>
Change
</button>
</div>
);
}
}
small, but imho nice simplification
also, you could use concise syntax with your mapDispatch:
const mapDispatch = dispatch => ({
changeName(name) {
dispatch({ type: "setName", name });
}
});
you can do this if you want to to add the typing text in store:
Settings.js
import React from "react";
import { changeName, typingName } from '../actions/settingsActions'
import { connect } from "react-redux";
const Settings = () => {
const { changeName, typingName, typedName, submittedName } = this.props
return (
<div>
<h1>Settings</h1>
<p>This is Settings page</p>
My name is{" "}
<input
value={typedName}
onChange={e => typingName(e.target.value)}/>
<button onClick={changeName(submittedName)}>
Change
</button>
</div>
);
}
const mapState = state => ({
typedName: state.typedName,
submittedName: state.submittedName
});
const mapDispatchToProps = dispatch => ({
typingName: x => dispatch(typingName(x)),
changeName: x => dispatch(changeName(x))
})
export default connect(
mapState,
mapDispatch
)(Settings);
settingsActions.js
export const typingName = payload => ({
type: 'TYPING_NAME',
payload
});
export const changeName = payload => ({
type: 'CHANGE_NAME',
payload
});
settingsReducer.js
export const typingName = (state = [], action) => {
switch (action.type) {
case 'TYPING_NAME':
return [...state, action.payload];
default:
return state;
}
};
export const changeName = (state = '', action) => {
switch (action.type) {
case 'CHANGING_NAME':
return action.payload;
default:
return state;
}
};
You could maybe achieve something like this. But validating the typing state inside the component then sending the final result to the store as you did is a better idea I think, to avoid so much verbose.
Also you should of course create a constants file, but I guess you know already.
I'm trying to get to grips with Redux + React - I have hooked up the relevant bits of Redux with connect() for a small todo app but I cannot for the life of me get the component to update and show the reflected store changes. The store state does update however the component will not. Here are the relevant bits in my code:
actionTypes.js
export const ADD_TODO = "ADD_TODO";
export const DELETE_TODO = "DELETE_TODO";
export const CLEAR_TODO = "CLEAR_TODO";
export const COMPLETE_TODO = "COMPLETE_TODO";
reducers.js
import {ADD_TODO, COMPLETE_TODO, DELETE_TODO, CLEAR_TODO} from '../actions/actionTypes';
const todoApp = (state, action) => {
let updatedState;
switch (action.type) {
case ADD_TODO:
updatedState = Object.assign({}, state);
updatedState.todo.items.push({
text: action.text,
completed: false
});
return updatedState;
case COMPLETE_TODO:
updatedState = Object.assign({}, state);
updatedState.todo.items[action.index].completed = true;
return updatedState;
case DELETE_TODO:
const items = [].concat(state.todo.items);
items.splice(action.index, 1);
return Object.assign({}, state, {
todo: {
items: items
}
});
case CLEAR_TODO:
return Object.assign({}, state, {
todo: {
items: []
}
});
default:
return state;
}
};
export default todoApp;
actions.js
import {ADD_TODO, COMPLETE_TODO, DELETE_TODO, CLEAR_TODO} from './actionTypes.js';
export const addTodoCreator = (text) => {
return {
type: ADD_TODO,
text: text,
completed: false
}
};
export const completeTodo = (index) => {
return {
type: COMPLETE_TODO,
index: index
}
};
export const deleteTodo = (index) => {
return {
type: DELETE_TODO,
index: index
}
};
export const clearTodo = (index) => {
return {
type: CLEAR_TODO,
index: index
}
};
AddTodoContainer.js
import { connect } from 'react-redux';
import TodoList from '../components/TodoList';
const mapStateToProps = (state, ownProps) => {
return {
todo: state.todo
}
};
export default connect(mapStateToProps)(TodoList);
TodoListContainer.js
import { connect } from 'react-redux';
import {addTodoCreator} from '../actions/actions';
import AddTodo from '../components/AddTodo';
const mapStateToProps = (state) => {
console.log(state);
return {
todo: state.todo
}
};
const mapDispatchToProps = (dispatch) => {
return {
addTodo: (text) => {
const action = addTodoCreator(text);
dispatch(action);
},
}
};
export default connect(mapStateToProps, mapDispatchToProps)(AddTodo);
AddTodo.js
import React from 'react'
const handler = (addTodo) => {
const text = document.getElementById('textInput').value;
addTodo(text);
};
const AddTodo = ({addTodo}) => {
return (
<div>
<input id="textInput" type="text" className="textInput" />
<button onClick={(handler).bind(null, addTodo)}>Add</button>
</div>
)
}
export default AddTodo
TodoList.js
import React from 'react';
import AddTodoContainer from '../containers/AddTodoContainer';
class TodoList extends React.Component {
render () {
console.log(this.props);
return (
<div>
<ul>
{this.props.todo.items.map((item) => {
return <li>
{item.text}
</li>
})}
</ul>
<AddTodoContainer/>
</div>
)
}
}
export default TodoList;
I've tried all of the suggestions under Troubleshooting and as far as I can tell I am not mutating state. The reducer is firing and I can log out the states. The code is stored here under react-fulltodo http://gogs.dev.dylanscott.me/dylanrhysscott/learn-redux
Thanks
Dylan
You're passing todo to your component and while the todo object gets updated the actual todo object in redux state is the same exact object as it was before. So react does not see the object as changed. For example:
const a = { foo: 'bar' };
const b = a;
b.foo = 'I made a change';
console.log(a==b);
// logs true because a and b are the same object
// This is exactly what's happening in React.
// It sees the object as the same, so it does not update.
You need to clone the todo object so that react sees it as a changed/new object.
In your reducer:
switch (action.type) {
case ADD_TODO:
updatedState = Object.assign({}, state);
// Shallow clone updatedState.todo
updatedState.todo = Object.assign({}, updatedState.todo);
updatedState.todo.items.push({
text: action.text,
completed: false
});
return updatedState;
Meanwhile, if you passed state.todo.items to your component you would not have to clone todo but you would have to clone items. So in the future, if you have a component that directly mapStateToProps with state.todo.items, it will have the same problem because you are not cloning the items array in ADD_TODO like you are in the DELETE_TODO reducer.