I have a connected top level component :
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import store from './store/store';
import { Provider } from 'react-redux';
import LifePlanner from './lifePlanner';
import { getContainer, setContainer } from './store/plannerSlice'
class App extends React.Component {
componentDidMount() {
fetch("/lp/top-layer", { mode: 'cors' }).then((response) => {
if (response.status !== 200) {
console.log('Looks like there was a problem. Status Code: ' +
response.status);
return;
}
response.json().then((data) => {
store.dispatch(setContainer({ name: "Root Container", children: data.data.containers }));
this.setState({ container: getContainer(store.getState()) })
});
}
).catch(function (err) {
console.log('Fetch Error :-S', err);
});
}
render() {
let c = this.state.container;
if (c.name) {
return <LifePlanner container={c} />
}
return <h1>Loading</h1>;
}
}
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>
,
document.getElementById('root')
);
LifePLanner then calls an ExpandedCard component, psasing the container down and then again to a ImageCard component where I put an onClick, which changes the openContainer item in the store
import React from 'react';
import { makeStyles } from '#material-ui/core/styles';
import Card from '#material-ui/core/Card';
import CardActionArea from '#material-ui/core/CardActionArea';
import CardActions from '#material-ui/core/CardActions';
import CardContent from '#material-ui/core/CardContent';
import CardMedia from '#material-ui/core/CardMedia';
import Button from '#material-ui/core/Button';
import Typography from '#material-ui/core/Typography';
import { setContainer } from '../store/plannerSlice'
import store from '../store/store';
const useStyles = makeStyles({
media: { height: 250 },
});
export default function SectionCard({ id, name, image, description, children }) {
const classes = useStyles();
const handleClick = () => {
fetch(`/lp?${id}`, { mode: 'cors' }).then((response) => {
if (response.status !== 200) {
console.log('Looks like there was a problem. Status Code: ' +
response.status);
return;
}
response.json().then((data) => {
store.dispatch(setContainer({ name , children: data.data.containers }));
});
}
).catch(function (err) {
console.log('Fetch Error :-S', err);
});
}
return (
<Card className={classes.root} onClick={()=>handleClick()}>
<CardActionArea>
{image &&
<CardMedia
className={classes.media}
image={image}
title="Contemplative Reptile"
/>}
<CardContent>
<Typography gutterBottom variant="h5" component="h2">
{name}
</Typography>
<Typography variant="body2" color="textSecondary" component="p">
{description}
</Typography>
</CardContent>
</CardActionArea>
{(children && children.length > 0) &&
<CardActions>
<Button size="small" color="primary">
Expand (Contains {children.length} items)
</Button>
</CardActions>
}
</Card>
);
}
Heres my reducer too:
import { createSlice } from '#reduxjs/toolkit';
export const slice = createSlice({
name: 'containers',
initialState: {
openContainer: {}
},
reducers: {
setContainer: (state, action) => {
let payload = action.payload;
state.openContainer = {...payload}
},
},
});
export const getContainer = state => {
return state.containers.openContainer
};
export const { setContainer } = slice.actions;
export default slice.reducer;
None of the components get their componentWilLReceiveProps triggered or get notified of the change, but the store does get the change from the click
Yes, there's several issues here. The primary problems are:
You're interacting with the store directly in our components. Instead, you should be using the React-Redux APIs like connect and useSelector/useDispatch to read data from the store and dispatch actions in your components. That way they'll update properly when the store state gets updated.
You shouldn't be copying data from props to state just to render with it. Copying data from props to state is almost always the wrong approach.
A few other notes:
Conceptually, think of actions as "events that happened", not "setters". Instead of calling that reducer/action setContainer, call it something like containerDataLoaded.
You don't need to spread {...payload} in that reducer, since the object was constructed when you dispatched the action. You can simplify that line to state.openContainer = action.payload.
You might want to consider using async/await syntax instead of promise chaining.
Related
i am trying to test if a like counter has been increased after user clicks a button. I am using react testing library where i find a button and preform userEvent.click this should dispatch an action behind the scenes and increment the counter where i can then assert the new value.
This works when i manually go through the ui but cannot get the test to work.
Button:
<Button
size="small"
color="primary"
onClick={() => dispatch(likePosts(post._id))}
>
<ThumbUpIcon fontSize="small" />
Like {`${post.likeCount}`}
{}
</Button>
Thunk action:
export const likePosts = (id) => async (dispatch) => {
try {
const { data } = await api.likePost(id);
dispatch({ type: LIKE, payload: data });
} catch (error) {
console.log(error);
}
I have also set up a test-util to help me test connected component TEST UTIL LINK
I have also added applyMiddleware(thunk) to support thunk when a for a connected component
test-util:
import React from "react";
import { render as rtlRender } from "#testing-library/react";
import { legacy_createStore, applyMiddleware } from "redux";
import { Provider } from "react-redux";
import thunk from "redux-thunk";
// Replace this with the appropriate imports for your project
import reducers from "../redux/reducers";
const render = (
ui,
{
store = legacy_createStore(reducers, applyMiddleware(thunk)),
...renderOptions
} = {}
) => {
const Wrapper = ({ children }) => (
<Provider store={store}>{children}</Provider>
);
return rtlRender(ui, { wrapper: Wrapper, ...renderOptions });
};
export * from "#testing-library/react";
export * from "#testing-library/jest-dom";
// override render method
export { render };
My Test:
import Post from "./Post";
import { render, screen } from "../../../utils/test-utils";
import userEvent from "#testing-library/user-event";
describe("Post", () => {
let initialState;
beforeEach(() => {
initialState = {
_id: "1234",
title: "post title",
message: "post message",
creator: "post creator",
tags: ["postTag", "postTag"],
selectedFile: "path/to/file",
likeCount: 0,
createdAt: "2022-07-20T23:54:25.251Z",
};
});
test("should increment post likes when like button clicked", () => {
render(<Post post={initialState} />, { initialState });
const postLikeButton = screen.getByRole("button", { name: /Like/i });
userEvent.click(postLikeButton);
const clickedPostLikeButton = screen.getByRole("button", {
name: /Like 1/i,
}).textContent;
// expect().toHaveBeenCalled();
expect(clickedPostLikeButton).toBe(100);
});
});
Test error:
TestingLibraryElementError: Unable to find an accessible element with the role "button" and name `/Like 1/i`
That would suggest that the the action was not dispatched when then button was clicked in the test.
UPDATE:
The button is from MUI library:
import { Button } from "#material-ui/core";
The post prop is passed from its parent component Posts:
import React from "react";
import { useSelector } from "react-redux";
import { Grid, CircularProgress } from "#material-ui/core";
import Post from "./Post/Post";
import useStyles from "./styles";
const Posts = ({ setCurrentId }) => {
const posts = useSelector((state) => state.posts);
const classes = useStyles();
return !posts.length ? (
<CircularProgress />
) : (
<Grid
className={classes.container}
container
alignItems="stretch"
spacing={3}
>
{posts.map((post, index) => (
<Grid key={index} item xs={12} sm={6}>
<Post key={post.id} post={post} setCurrentId={setCurrentId} />
</Grid>
))}
</Grid>
);
};
export default Posts;
Also all of this works just fine when using the UI, its just in react testing library test the button onClick seems the not to dispatch likePosts
Did you try with redux-mock-store?
import configureStore from 'redux-mock-store'
const mockStore = configureStore()
const store = mockStore(reducers) // add your reducers here
// ...
render(
<Provider store={store}>
{children}
</Provider>
)
I have a list of patients. I need to display the data of a single patient. The patients are identified uniquely using an id. When I get a particular patient's data from the backend using axios, it seems to work perfectly. But the problem is that I need the state to update to that object when I click on the patient's link. When I log from the reducer.ts file, the payload loads perfectly. But then, when I click on the link to actually show the data, it does not show it until the page is refreshed. However, after it is refreshed it does change the state to the unique object I want, and then reverts back to the initial state. I want it to get the object and keep it in the state. Where am I going wrong?
Before the page is refreshed:
After the page is refreshed:
index.ts:
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { reducer, StateProvider } from "./state";
ReactDOM.render(
<StateProvider reducer={reducer}>
<App />
</StateProvider>,
document.getElementById('root')
);
Reducer.ts:
import { State } from "./state";
import { Patient } from "../types";
export type Action =
| {
type: "SET_PATIENT_LIST";
payload: Patient[];
}
| {
type: "SINGLE_PATIENT";
payload: Patient;
};
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "SET_PATIENT_LIST":
return {
...state,
patients: {
...action.payload.reduce(
(memo, patient) => ({ ...memo, [patient.id]: patient }),
{}
),
...state.patients
}
};
case "SINGLE_PATIENT":
console.log(action.payload);
return {
...state,
patients: {
...state.patients,
[action.payload.id]: {
...state.patients[action.payload.id],
...action.payload,
},
}
};
default:
return state;
}
};
export const setPatientList = (patientList: Patient[]): Action => {
return {
type: "SET_PATIENT_LIST",
payload: patientList
};
};
export const setSinglePatient = (patient: Patient): Action => {
return {
type: "SINGLE_PATIENT",
payload: patient
};
};
State.tsx:
import React, { createContext, useContext, useReducer } from "react";
import { Patient } from "../types";
import { Action } from "./reducer";
export type State = {
patients: { [id: string]: Patient };
};
const initialState: State = {
patients: {}
};
export const StateContext = createContext<[State, React.Dispatch<Action>]>([initialState, () => initialState]);
type StateProviderProps = {
reducer: React.Reducer<State, Action>;
children: React.ReactElement;
};
export const StateProvider = ({
reducer,
children
}: StateProviderProps) => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<StateContext.Provider value={[state, dispatch]}>
{children}
</StateContext.Provider>
);
};
export const useStateValue = () => useContext(StateContext);
App.tsx:
import React from "react";
import axios from "axios";
import { BrowserRouter as Router, Route, Link, Routes } from "react-router-dom";
import { Button, Divider, Container, Typography } from "#material-ui/core";
import { apiBaseUrl } from "./constants";
import { useStateValue, setPatientList } from "./state";
import { Patient } from "./types";
import PatientListPage from "./PatientListPage";
import SinglePatientPage from "./SinglePatientPage";
const App = () => {
const [, dispatch] = useStateValue();
React.useEffect(() => {
void axios.get<void>(`${apiBaseUrl}/ping`);
const fetchPatientList = async () => {
try {
const { data: patientListFromApi } = await axios.get<Patient[]>(
`${apiBaseUrl}/patients`
);
dispatch(setPatientList(patientListFromApi));
} catch (e) {
console.error(e);
}
};
void fetchPatientList();
}, [dispatch]);
return (
<div className="App">
<Router>
<Container>
<Typography variant="h3" style={{ marginBottom: "0.5em" }}>
Patientor
</Typography>
<Button component={Link} to="/" variant="contained" color="primary">
Home
</Button>
<Divider hidden />
<Routes>
<Route path="/" element={<PatientListPage />} />
<Route path="/patients/:id" element={<SinglePatientPage />} />
</Routes>
</Container>
</Router>
</div>
);
};
export default App;
PatientListPage.tsx:
import React from "react";
import axios from "axios";
import {
Box,
Table,
Button,
TableHead,
Typography,
TableCell,
TableRow,
TableBody
} from "#material-ui/core";
import { PatientFormValues } from "../AddPatientModal/AddPatientForm";
import AddPatientModal from "../AddPatientModal";
import { Patient } from "../types";
import { apiBaseUrl } from "../constants";
import HealthRatingBar from "../components/HealthRatingBar";
import { useStateValue } from "../state";
import { Link } from 'react-router-dom';
const PatientListPage = () => {
const [{ patients }, dispatch] = useStateValue();
const [error, setError] = React.useState<string>();
return (
<div className="App">
<Box>
<Typography align="center" variant="h6">
Patient list
</Typography>
</Box>
<Table style={{ marginBottom: "1em" }}>
<TableHead>
<TableRow>
<TableCell>Name</TableCell>
<TableCell>Gender</TableCell>
<TableCell>Occupation</TableCell>
<TableCell>Health Rating</TableCell>
</TableRow>
</TableHead>
<TableBody>
{Object.values(patients).map((patient: Patient) => (
<TableRow key={patient.id}>
<TableCell>
<Link to={`/patients/${patient.id}`}>
{patient.name}
</Link>
</TableCell>
<TableCell>{patient.gender}</TableCell>
<TableCell>{patient.occupation}</TableCell>
<TableCell>
<HealthRatingBar showText={false} rating={1} />
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
};
export default PatientListPage;
SinglePatientPage:
import React from "react";
import { Patient } from "../types";
import { useStateValue, setSinglePatient } from "../state";
import { useParams } from "react-router-dom";
import { Typography } from "#material-ui/core";
import { apiBaseUrl } from "../constants";
import axios from "axios";
const SinglePatientPage = () => {
const [{ patients }, dispatch] = useStateValue();
const { id } = useParams<{ id: string }>();
React.useEffect(() => {
const fetchSinglePatient = async () => {
if(id !== undefined) {
try {
const { data: patientFromApi } = await axios.get<Patient>(
`${apiBaseUrl}/patients/${id}`
);
dispatch(setSinglePatient(patientFromApi));
} catch (e) {
console.error(e);
}
}
};
void fetchSinglePatient();
}, [dispatch]);
if (patients) {
console.log('inside singlepatientpage', patients);
return (
<div className="app">
<Typography variant="h6" style={{ marginBottom: "0.5em" }}>
{patient.name}
<p>ssn: {patient.ssn}</p>
<p>occupation: {patient.occupation}</p>
</Typography>
</div>
);
}
return null;
};
export default SinglePatientPage;
It appears the SinglePatientPage component is missing using the id route param as a dependency for fetching the specific patient record.
Add id to the useEffect hook's dependency array since it is referenced in the callback.
const SinglePatientPage = () => {
const [{ patients }, dispatch] = useStateValue();
const { id } = useParams<{ id: string }>();
React.useEffect(() => {
const fetchSinglePatient = async () => {
if(id !== undefined) {
try {
const { data: patientFromApi } = await axios.get<Patient>(
`${apiBaseUrl}/patients/${id}` // <-- id referenced here
);
dispatch(setSinglePatient(patientFromApi));
} catch (e) {
console.error(e);
}
}
};
void fetchSinglePatient();
}, [dispatch, id]); // <-- add id dependency
if (!patients) {
return null;
}
return (
<div className="app">
<Typography variant="h6" style={{ marginBottom: "0.5em" }}>
{patient.name}
<p>ssn: {patient.ssn}</p>
<p>occupation: {patient.occupation}</p>
</Typography>
</div>
);
};
You might consider adding the React hooks eslint rules to your project to help catch missing dependencies like this in the future.
I figured out what the problem was. I hadn't made a new state for singular patient data.
export type State = {
patients: { [id: string]: Patient };
patient: Patient | null; //new state
};
const initialState: State = {
patients: {},
patient: null //new initial state
};
And in the reducer, I was returning an array of patients. Now I can return a single patient's data.
case "SINGLE_PATIENT":
return {
...state,
patient: action.payload // returning "patient" instead of "patients"
};
I have two pages; the first one called QuizHomePage and which contains a welcome message and a button which allows to user to start a quiz.
QuizHomePage.tsx:
import Button from "#material-ui/core/Button";
import { createStyles, makeStyles, Theme } from "#material-ui/core/styles";
import Typography from "#material-ui/core/Typography";
import React from "react";
import { connect } from "react-redux";
import { Dispatch } from "redux";
import { questionRequest, startQuiz } from "../../actions/index";
import AppBar from "../../components/common/AppBar";
import history from "../../history/history";
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
textAlign: "center",
margin: theme.spacing(10)
},
button: {
marginTop: theme.spacing(6)
}
}));
interface IProps {
questionRequest: () => void;
startQuiz: () => void;
}
const QuizHomePage = (props: IProps) => {
const classes = useStyles();
const { questionRequest, startQuiz } = props;
const handleStartQuiz = () => {
questionRequest();
startQuiz();
return history.push("/contentQuiz");
};
return (<>
<AppBar />
<div className={classes.root}>
<Typography
color="textPrimary"
gutterBottom
variant="h2">
Test your javascript skills
</Typography>
<Typography
color="textSecondary"
gutterBottom
variant="h6">
Please click the start button to launch the Quiz
</Typography>
<Button
className={classes.button}
color="secondary"
onClick={handleStartQuiz}
variant="contained">Start</Button>
</div>
</>);
};
const mapDispatchToProps = (dispatch: Dispatch) => {
return {
startQuiz: () => dispatch(startQuiz()),
questionRequest: () => dispatch<any>(questionRequest())
};
};
export default connect(null, mapDispatchToProps)(QuizHomePage);
When I click the Start button I dispatch 2 actions questionRequest which executes a promise and return the list of all questions from the database and startQuiz which dispatch an action to update the state of the quiz, then the user will be redirected to the quiz question page which described by this code:
import { Typography } from "#material-ui/core";
import React from "react";
import { connect } from "react-redux";
import SyntaxHighlighter from "react-syntax-highlighter";
import { dark } from "react-syntax-highlighter/dist/esm/styles/prism";
import { incrementQuestion, IQuestion } from "../../actions/index";
import ContentQuiz from "../../components/ContentQuiz";
interface IProps {
currentQuestionNumber: number;
questions: IQuestion[];
}
const QuizzContainer = (props: IProps) => {
const { currentQuestionNumber, questions } = props;
const currentQuestion = questions[currentQuestionNumber];
const handleNextQuiz = () => {
incrementQuestion();
};
return (
<ContentQuiz
questionNumber={currentQuestionNumber}
handleClick={handleNextQuiz}>
<div>
<Typography variant="h3" gutterBottom> What's the output of </Typography>
<>
<SyntaxHighlighter language="javascript" style={dark}>
{currentQuestion.questionDescription}
</SyntaxHighlighter>
</>
</div>
</ContentQuiz>
);
};
const mapStateToProps = (state: any) => {
const { currentQuestionNumber, questions } = state.quiz;
return {
currentQuestionNumber,
questions
};
};
export default connect(mapStateToProps, { incrementQuestion })(QuizzContainer);
actions.ts:
export const questionRequest = (): ThunkAction<void, AppState, null, Action<string>> => {
return async (dispatch: Dispatch) => {
dispatch(startQuestionRequest());
getQuestionsApi().then((response: AxiosResponse) => {
const { data } = response;
dispatch(questionSuccess(data.result));
},
(error: AxiosError) => {
let errorMessage = "Internal Server Error";
if (error.response) {
errorMessage = error.response.data.error;
}
dispatch(questionFail(errorMessage));
dispatch(errorAlert(errorMessage));
});
};
};
I got an error :
TypeError: Cannot read property 'questionDescription' of undefined
it's normally because for react the questionsvariable is undefined. I realized that the questions array is not updated quickly but after some amount of time due to the server response that's why the QuizzContainerreturns the error mentioned below when it tries to mount the component.
Is it a good approach to lazy load the component in order to wait the fetching of questions from server and then mounting the QuizContainer component? I trieduseEffectwhich normally behaves ascomponentDidMount` but it does not work with my issue.
How can I fix that?
You need to use async and await here. If you don't wait until the promise gets resolved and navigate the user to the next page, you can never guarantee that the user will see the question as soon as page loads.
const handleStartQuiz = async () => {
awit questionRequest();
await startQuiz();
return history.push("/contentQuiz");
}
Second approach: (I don't recommend)
Don't render the question unless you have questions filled in the redux state.
return(
{ questions && <ContentQuiz> ... </ContentQuiz> }
)
I resolved my question using this update:
import { Typography } from "#material-ui/core";
import React from "react";
import { connect } from "react-redux";
import SyntaxHighlighter from "react-syntax-highlighter";
import { dark } from "react-syntax-highlighter/dist/esm/styles/prism";
import { incrementQuestion, IQuestion } from "../../actions/index";
import ContentQuiz from "../../components/ContentQuiz";
interface IProps {
currentQuestionNumber: number;
loadingData: boolean;
questions: IQuestion[];
questionRequest: () => void;
}
const QuizzContainer = (props: IProps) => {
const { currentQuestionNumber, loadingData, questions, questionRequest } = props;
useEffect(() => {
questionRequest();
});
const currentQuestion = questions[currentQuestionNumber];
const handleNextQuiz = () => {
incrementQuestion();
};
return (
<div>
{loadingData ? ("Loading ...") : (
<ContentQuiz
questionNumber={currentQuestionNumber}
handleClick={handleNextQuiz}>
<div>
<Typography variant="h3" gutterBottom> What's the output of </Typography>
<>
<SyntaxHighlighter language="javascript" style={dark}>
{currentQuestion.questionDescription}
</SyntaxHighlighter>
</>
</div>
</ContentQuiz>
)}
</div>
);
};
const mapStateToProps = (state: any) => {
const { currentQuestionNumber, loadingData, questions } = state.quiz;
return {
currentQuestionNumber,
loadingData,
questions
};
};
const mapDispatchToProps = (dispatch: Dispatch) => {
return {
incrementQuestion: () => dispatch(incrementQuestion()),
questionRequest: () => dispatch<any>(questionRequest())
};
};
export default connect(mapStateToProps, mapDispatchToProps)(QuizzContainer);
I'm struggling to get a basic api call setup with redux and axios in React Native.
This is my reducer index.js
import { combineReducers } from 'redux'
import LibaryReducer from './LibraryReducer'
import ImportLibraryReducer from './ImportLibraryReducer'
let defaultState = {
card: null
}
const mainReducer = (state = defaultState, action) => {
if(action.type === "CHANGE_CARDS") {
return {
...state,
card: action.card
}
} else {
return {
...state
}
}
}
export default mainReducer
This is my action index.js
import axios from "axios"
export function loadCards(){
return(dispatch)=>{
return axios.get('http://localhost:4000/reports')
.then(response => {
dispatch(changeCards(response.data))
})
}
}
export function changeCards(cards) {
return{
type: "CHANGE_CARDS",
card: card
}
}
This is my app.js
/**
* Sample React Native App
* https://github.com/facebook/react-native
*
* #format
* #flow
*/
import React, {Component} from 'react';
import {Platform, StyleSheet, Text, View} from 'react-native';
import MainPage from './components/MainPage'
import { Header } from "native-base"
import Card from './components/Card'
import { Provider } from 'react-redux'
import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import reducers from './reducers'
const store = createStore(reducers, applyMiddleware(thunk))
type Props = {};
export default class App extends Component<Props> {
render() {
return (
<Provider store={store}>
<View>
<Header ><Text>hello</Text></Header>
<Card />
</View>
</Provider>
);
}
}
And, finally, this is where I'm trying to retrieve the data from the api call:
import React, { Component } from 'react';
import { Text, View } from 'react-native';
import {Collapse,CollapseHeader, CollapseBody, AccordionList} from 'accordion-collapse-react-native';
import { connect } from 'react-redux'
import * as actions from '../actions'
class Card extends Component {
render() {
const titleStyle = {
backgroundColor: '#edeeef',
fontWeight: "bold",
color: '#454647',
fontSize: 16,
left: 8,
fontFamily: 'Ionicons',
top: 10
}
const descMarkStyle = {
left: 8,
top: 4,
fontFamily: 'Ionicons',
color: '#454647',
fontSize: 16
}
console.log('in the render', this.props)
return (
<View>
<Collapse >
<CollapseHeader>
<View
style={{
backgroundColor: '#edeeef',
height: 38,
postion: 'absolute',
borderBottomWidth: .5,
borderBottomColor: '#black'
}}
>
<Text style={titleStyle}>
test
</Text>
</View>
</CollapseHeader>
<CollapseBody>
<Text style={descMarkStyle}>test</Text>
<Text style={descMarkStyle}>test</Text>
</CollapseBody>
</Collapse>
</View>
);
}
}
function mapStateToProps(state) {
return {
state
};
}
export default connect(mapStateToProps)(Card);
When I try to console log this.props in the component above, I get the default state of card: null without the api running: https://imgur.com/a/acB40KU
I'm new to redux, and I feel like there is something obvious that I'm missing.
You should trigger your action in the componentDidMount lifecycle method in your Card component. Also, you can destructure your actions in your imports and in your connect.
import { loadCards } from '../actions'
class Card extends Component {
componentDidMount() {
this.props.loadCards()
}
And in connect:
export default connect(mapStateToProps, { loadCards })(Card);
Also in the changeCards action:
card: cards
Here is how to set up axios with redux hooks and react-native in 4 steps:
source code: https://github.com/trackmystories/Redux-hooks-counter-app-with-axios.
Step 1:
create an actions.js file:
actions.js
export const TOTAL_COUNT = "TOTAL_COUNT";
export const totalCount = (data) => ({
type: TOTAL_COUNT,
data,
});
Step 2:
define and combine your reducers:
reducer.js
import { combineReducers } from "redux";
import { TOTAL_COUNT } from "./actions";
let dataState = { data: [] };
const total_counts = (state = dataState, action) => {
switch (action.type) {
case TOTAL_COUNT:
return { ...state, data: action.data };
default:
return state;
}
};
const counter = (state = 0, action) => {
switch (action.type) {
case "ADD":
return state + 1;
case "SUBTRACT":
return state - 1;
default:
return state;
}
};
const rootReducer = combineReducers({
counter,
total_counts,
});
export default rootReducer;
Step 3
create an axios get request and put request as defined in the example below and dispatch and get data.
With hooks you don't need to use connect mapStateToProps and dispatchStateToProps with redux hooks instead use { useDispatch, useSelector }.
We can pass the actions "ADD" and "SUBTRACT" inside of the button directly, without defining an action.js file.
CounterComponent.js
import React, { useEffect, useState } from "react";
import { StyleSheet, Text, View, ActivityIndicator } from "react-native";
import ActionButton from "./ActionButton";
import SubmitButton from "./SubmitButton";
import { useDispatch, useSelector } from "react-redux";
import axios from "axios";
import { totalCount } from "../actions";
export default function CounterComponent() {
const dispatch = useDispatch();
const [isFetching, setIsFetching] = useState(false);
const total_counts = useSelector((state) => state.total_counts);
const counter = useSelector((state) => state.counter);
const { data } = total_counts;
useEffect(() => getTotalCount(), []);
const getTotalCount = () => {
setIsFetching(true);
let url = "https://url.firebaseio.com<name>.json";
axios
.get(url)
.then((res) => res.data)
.then((data) => dispatch(totalCount(data)))
.catch((error) => alert(error.message))
.finally(() => setIsFetching(false));
};
const onSubmit = (counterState) => {
let url = "https://url.firebaseio.com<name>.json";
axios.put(url, counterState).then((response) => {
console.log(response);
});
};
return (
<View>
<ActionButton
onPress={() =>
dispatch({
type: "SUBTRACT",
})
}
title="subtract"
/>
<View>
{isFetching ? (
<ActivityIndicator />
) : (
<View>
<Text>
Current state:
{data.counter ? data.counter : counter}
</Text>
</View>
)}
</View>
<ActionButton
onPress={() =>
dispatch({
type: "ADD",
})
}
title="add"
/>
<SubmitButton
onPress={onSubmit({
counterState: counter,
})}
title="Submit"
/>
</View>
);
}
Step 4:
Lastly link your RootReducer to the createStore and pass it to the Provider.
import React from "react";
import { Text, View } from "react-native";
import { Provider } from "react-redux";
import { createStore } from "redux";
import CounterComponent from "./src/components/CounterComponent";
import rootReducer from "./src/reducer";
const store = createStore(rootReducer);
export default function App() {
return (
<View>
<Text>Counter example with Redux Hooks and Axios</Text>
<Provider store={store}>
<CounterComponent />
</Provider>
</View>
);
}
I know this is common issue and have been asked many times but i have gone through every solution but it didn't work.
I have been facing this error when i try to login from login form.
Here's code i'm attaching.
Login.js (View)
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { ScrollView, Text, TextInput, View, Button,StyleSheet,TouchableOpacity } from 'react-native';
import {loginRequest} from './../redux/actions/auth';
import {bindActionCreators} from 'redux';
class Login extends Component {
constructor (props) {
super(props);
this.state = {
username: '',
password: ''
};
}
userLogin (e) {
this.props.actions.loginRequest(this.state.username, this.state.password);
}
render () {
return (
<ScrollView style={{padding: 20,backgroundColor:'#ccc'}}>
<View style = {styles.container}>
<View style={{marginLeft:15}}>
<Text>Email</Text>
</View>
<TextInput
style = {styles.input}
underlineColorAndroid = "transparent"
placeholder = "Enter username"
placeholderTextColor = "#9a73ef"
autoCapitalize = "none"
onChangeText={(text) => this.setState({ username: text })}/>
<View style={{marginLeft:15}}>
<Text>Password</Text>
</View>
<TextInput
style = {styles.input}
underlineColorAndroid = "transparent"
placeholder = "Enter Password > 6 letters"
placeholderTextColor = "#9a73ef"
autoCapitalize = "none"
onChangeText={(text) => this.setState({ password: text })}/>
<TouchableOpacity
style = {styles.submitButton}
onPress={(e) => this.userLogin(e)}>
<Text style = {styles.submitButtonText}> Submit </Text>
</TouchableOpacity>
</View>
</ScrollView>
);
}
}
const mapStateToProps = (state, ownProps) => {
return {
isLoggedIn: state.auth.isLoggedIn
};
}
function mapDispatchToProps(dispatch){
return {
actions : bindActionCreators({
loginRequest
},dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Login);
auth action
In this action i am calling dispatch without any api call directly just for testing but even though that's not working.
export function loginRequest(username,password) {
alert("TEst"); // this alert comes
return function (dispatch) {
alert(`........Tracker......`); // execution doesn't reach here ,this alert doesn't com
if(username == 'admin' && password == 'admin'){
alert(`Login Success......`);
dispatch({
type : 'LOGIN_SUCCESS',
msg : 'Logged in successfully.'
});
resolve(true);
} else {
alert(`Login Failed......`);
dispatch({
type : 'LOGIN_FAIL',
msg : 'Please make sure you have entered valid credentials.'
})
reject(false);
}
};
}
This is my authReducer.js
export default function reducer(state = {},action){
if(action.type == 'LOGIN_SUCCESS'){
alert('login success');
return Object.assign({},state,{
isLoggedIn:true
})
} else if(action.type == 'LOGIN_FAIL'){
alert('login failed');
return Object.assign({},state,{
isLoggedIn:false
})
} else {
return state;
}
}
And entry point
import React, { Component } from 'react';
import {
Platform,
StyleSheet,
Text,
View
} from 'react-native';
import {Provider} from 'react-redux';
import store from './redux';
import Application from './pages/Application';
export default class App extends Component{
render() {
return (
<Provider store={store}>
<Application />
</Provider>
);
}
}
I couldn't find any solution, can anyone help?
Because you are returning a function from your actions, you need to use middleware to handle returning functions instead of objects.
I am a fan of redux-thunk but there are plenty of other redux middlewares out there for this exact purpose.
You will need to update your store and configure it to use the middleware like so:
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers';
export default(initialState) => {
return createStore(rootReducer, initialState, applyMiddleware(thunk));
}