I am creating a simple Todo app using React JS,Hooks and redux.As per the below code Why can't I update state and retrieve the data ? When I submit the input fields it doesn't show error but I can't retrieve data from Redux state . Mostly there may be error with usage of useSelector function and initialState.
# necessary imports,omitted for readers ease.
function FormComp() {
const dispatch = useDispatch()
const [inputdata,setInputdata] = useState('')
const handlesubmit =(e)=>{e.preventDefault();
dispatch(allActions.formAction.addTodo(inputdata))
}
const addtodo = useSelector(state=>state.todo)
return (
<div>
<form>
<input onChange={(e)=>setInputdata(e.target.value)} value={inputdata} />
<button onClick={handlesubmit}> submit </button>
</form>
{console.log(addtodo)} # console shows undefined
</div>
)
}
export default FormComp
REDUCER FUNCTION
import { ADDTODO } from "../constants/types";
const initialState={todo:'no todos'}
const toDos = (state=initialState,action)=>{
switch(action.type){
case ADDTODO:return{
...state,todo:action.payload
}
default:return state
}
}
export default toDos
ACTION FUNCTION
import { ADDTODO } from "../constants/types"
const addTodo=(todo)=>{return {
type:ADDTODO,
payload:todo
}}
export default{
addTodo
}
Related
Just stumbled upon the question of writing custom redux hooks.
Here is a backbone of the App.js
import { Provider, useStore, useDispatch } from "./redux";
import reducers from "./reducers";
import "./styles.css";
import React} from "react";
function Hello() {
const counter = useStore(store => store.counter);
const dispatch = useDispatch();
const increment = () => dispatch({ type: "INCREMENT" });
const decrement = () => dispatch({ type: "DECREMENT" });
return (
<div>
<h3>Counter: 0</h3>
<button onClick={increment}>+ Increment</button>
<button onClick={decrement}>- Decrement</button>
</div>
);
}
// <Provider reducers={reducers}>
export default function App() {
return (
<div className="App">
<Provider reducers={reducers}>
<Hello />
</Provider>
</div>
);
}
The idea is to write implementations for useStore, useDispatch and Provider.
I got the idea that we should use context api and useReducer to have access to dispatch but then I got stuck.
I know Provider can something be like
const initialState = {};
const context = createContext(initialState);
export function Provider({ children, reducers }) {
const [state, dispatch] = useReducer(reducers, initialState);
// children need to have access to dispatch
return (
<context.Provider value={{ state, dispatch }}>{children}</context.Provider>
);
}
And similarly we could have used useContext to have access to the value passed
like
export function useStore(selector) {
const { state} = useContext(context);
return { state.selector};
}
/**
* Returns the dispatch function
*/
export function useDispatch() {
const { dispatch } = useContext(context);
return { dispatch };
}
reducers file is also a part of the sandbox link given below.
But I am stuck and cannot figure why it's not working.
Please help and explain.
Here is the work in progress sandbox
https://codesandbox.io/s/redux-forked-tlb4hb?file=/src/redux.js
I am using context API in react for managing state. For this I have created a file AppContext.js where I have created context and Provider:
import { useState, createContext } from "react";
export const AppContext = createContext();
export const AppProvider = (props) => {
const [appdata, setAppdata] = useState({
data1: "this is data1",
data2: "this is data2",
});
return (
<AppContext.Provider value={[appdata, setAppdata]}>
{props.children}
</AppContext.Provider>
);
};
I have imported this Provider in the parent component of the app i.e App.js. Also I have wrapped the <AppChild/> component in the Provider.
import AppChild from "./AppChild";
import { AppProvider } from "./AppContext";
const App = () => {
return (
<AppProvider>
<div className="App">hello</div>
<AppChild />
</AppProvider>
);
};
export default App;
Now from AppChild component, I only needed to update the data1 key of my state. For this I have created a button with a onClick through which I will be changing my state. I have used to following code in AppChild.js for this:
import { useContext } from "react";
import { AppContext } from "./AppContext";
const AppChild = () => {
const [appdata, setAppdata] = useContext(AppContext);
return (
<div>
<h3 style={{ color: "red" }}>Data for Appchild: {appdata.data1}</h3>
<button
onClick={() =>
setAppdata((prev) => {
prev.data1 = "updated data1";
return prev;
})
}
>
click to change
</button>
</div>
);
};
export default AppChild;
But when I click the button, the text inside the <h3> block is not changing. Although when I change the whole state by passing the whole object directly to setAppdata as shown below,
This way the state updates successfully. Why is the first method not working where I only wanted to change the data1 key?
You are updating state in wrong way so it is not working. This is how you should update state
<button
onClick={() =>
setAppdata((prevState) => ({
...prevState,
data1: "Your New Data"
}))
}
>
click to change
</button>
First I would like to say to I've never worked with either next.js or the context api so please bear with me.
I'm currently working on a web application in Next.js where I have multiple pages that each contain a form. I would like to have a global state of some sort in order to be able to set and update the data from each form. All form data together
For example: page 1 = name, page 2 = description, ...
From what I've read online, I thought that using the context api would be sufficient, but I've hit a wall. When I fill in the name on the first form it doesn't get saved in the global state because it doesn't show up on the second page.
I don't understand where I went wrong so any help i more than welcome!
p.s. if i didn't explain some part right or forgot to add some code snippet please let me know.
businessContext.tsx
import { createContext, useState } from "react";
//accessible data
export interface BusinessContextData {
businessName: string;
handleBusinessName: (name: string) => void;
}
//default values
export const businessContextDefaultValue: BusinessContextData = {
businessName: "",
};
//provider
export const BusinessContext = createContext<BusinessContextData>(
businessContextDefaultValue
);
//hooks that components can use to change the values
export function useBusinessContextValue(): BusinessContextData {
const [businessName, setBName] = useState<string>("");
const handleBusinessName = (name: string) => {
setBName(name);
};
return {
businessName,
handleBusinessName,
};
}
_app.tsx
import type { AppProps } from "next/app";
import {
useBusinessContextValue,
BusinessContext,
} from "../context/businessContext";
import "../styles/global.css";
function MyApp({ Component, pageProps }: AppProps) {
const businessContextValue = useBusinessContextValue();
return (
<BusinessContext.Provider value={businessContextValue}>
<Component {...pageProps} />
</BusinessContext.Provider>
);
}
export default MyApp;
businessName.tsx - name form page (should save the given name in global state)
import { ChangeEvent, FormEvent, useContext, useState } from "react";
import { BusinessContext } from "../context/businessContext";
const IndexPage = () => {
const { handleBusinessName } = useContext(BusinessContext);
const router = useRouter();
const [businessNameState, setBusinessnameState] = useState<string>("");
const onSubmit = (e: FormEvent) => {
handleBusinessName(businessNameState);
router.push("/businessVision");
};
return (
...
<form onSubmit={(e: FormEvent) => onSubmit(e)}>
<div className="formInputRow">
<input
className="formInput"
type="text"
placeholder="Business name"
required
value={businessNameState}
onChange={(val: ChangeEvent<HTMLInputElement>) =>
setBusinessnameState(val.target.value)
}
/>
</div>
<button type="submit">
Next
</button>
</form>
...
);
};
export default IndexPage;
businessVision.tsx - should display business name from global state
import { ChangeEvent, FormEvent, useContext, useEffect, useState } from "react";
import { BusinessContext } from "../context/businessContext";
const BusinessVisionpage = () => {
const { businessName } = useContext(BusinessContext);
const router = useRouter();
const [businessVisionState, setBusinessVisionState] = useState<string>("");
return (
...
<h1>
<span>{businessName}</span>
</h1>
...
);
};
Your context is setup correctly, but you'll need to prevent the <form>'s submit default behaviour so that the navigation to the next page can happen properly.
const onSubmit = (e: FormEvent) => {
e.preventDefault(); // Prevents submit default behaviour
handleBusinessName(businessNameState);
router.push("/businessVision");
};
I'm trying to fetch and display data on the initial load of an application using react and redux. But the component that should display the data does not have the data by the time it is rendered. It eventually gets the data but doesn't re-render for some reason.
Here are my two components in question:
App.js
import React, {useEffect} from 'react';
import './App.css';
import RecordList from './components/RecordList';
import CreateRecord from './components/CreateRecord';
import {useDispatch} from 'react-redux';
import { initRecords } from './actions/recordActions';
function App() {
const dispatch = useDispatch();
// Gets initial record list.
useEffect(() => {
dispatch(initRecords());
}, [dispatch])
return (
<div className="App">
<CreateRecord />
<RecordList/>
</div>
);
}
export default App;
RecordList.js
import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
export default function RecordList() {
const dispatch = useDispatch();
const records = useSelector(state=>state);
console.log('state: ', records)
return (
<div>
<h3>Albums</h3>
{records.map(record =>
<div key={record.id}>
{record.albumName} by {record.artist}
</div>
)}
</div>
)
}
The issue I'm having is that initial data fetch in App.js isn't returning fast enough by the time the RecordList.js component is rendered. So in RecordList.js this bit throws an error saying map is not a function or cannot map on undefined:
{records.map(record =>
<div key={record.id}>
{record.albumName} by {record.artist}
</div>
)}
The component does eventually get the data if you comment out the JSX throwing the error. Initially it logs records as undefined but after a second it logs it with correct values.
Here are my reducer and actions:
recordActions.js
import recordService from '../services/records';
export const initRecords = () => {
return async dispatch => {
const records = await recordService.getAll();
console.log('from actions: ', records);
dispatch({
type: 'INIT_RECORDS',
data: records
})
};
}
reducer
const recordReducer = (state = [], action) => {
console.log('state now: ', state)
console.log('action', action)
switch(action.type) {
case 'CREATE':
return [...state, action.data];
case 'INIT_RECORDS':
return action.data;
default: return state;
}
}
export default recordReducer
Lastly, here is where I am making the axios call:
service
const getAll = async () => {
const response = await axios.get('someapi.com/records');
return response.data;
}
I've tried to conditionally render both the entire recordsList component and the records.map but the conditions only check once on the first load and never check again.
From my understanding, useSelector() should re-render the component when there's a state change, is it possible the state is just being mutated and not changed and how can I fix this?
Figured it out! Turns out in useSelector(state=>state) I needed to change it to useSelector(state=>state.records.Items) to get to the array.
I am breaking apart Redux' todo example to try to understand it. I read that mapDispatchToProps allows you to map dispatch actions as props, so I thought of rewriting addTodo.js to use mapDispatchToProps instead of calling dispatch(addTodo()). I called it addingTodo(). Something like this:
import React from 'react';
import {connect} from 'react-redux';
import addTodo from '../actions';
let AddTodo = ({addingTodo}) => {
let input;
return (
<div>
<form onSubmit={e => {
e.preventDefault()
if (!input.value.trim()) {
return
}
addingTodo(input.value)
input.value = ""
}}>
<input ref={node => {
input = node
}} />
<button type="submit">Submit</button>
</form>
</div>
)
}
const mapDispatchToProps = {
addingTodo: addTodo
}
AddTodo = connect(
mapDispatchToProps
)(AddTodo)
export default AddTodo
However, when I run the app, I get this error: Error: Invalid value of type object for mapStateToProps argument when connecting component AddTodo.. I never used mapStateToProps to begin with on AddTodo component, so I was not sure what was wrong. My gut feeling says that connect() expects mapStateToProps to precede mapDispatchToProps.
The working original looks like this:
import React from 'react';
import {connect} from 'react-redux';
import addTodo from '../actions';
let AddTodo = ({dispatch}) => {
let input;
return (
<div>
<form onSubmit={e => {
e.preventDefault()
if (!input.value.trim()) {
return
}
dispatch(addTodo(input.value))
input.value = ""
}}>
<input ref={node => {
input = node
}} />
<button type="submit">Submit</button>
</form>
</div>
)
}
AddTodo = connect()(AddTodo)
export default AddTodo
Complete repo can be found here.
So my question is, is it possible to do mapDispatchToProps without mapStateToProps? Is what I am trying to do an acceptable practice - if not, why not?
Yes, you can. Just pass null as first argument:
AddTodo = connect(
null,
mapDispatchToProps
)(AddTodo)
Yes, it's not just acceptable practice, it's recommended way to trigger actions. Using mapDispatchToProps allows to hide the fact of using redux inside your react components