I am new to writing test cases for React. Can someone tell me how to proceed with writing test cases for this file and how to finish code coverage.
How do i test mapDispatchToProps, componentDidMount or handleClick functions below. Can someone explain me how to proceed with steps to achieve test cases.
import React, { Component } from 'react'
import { connect } from 'react-redux'
import PropTypes from 'prop-types'
import DOMPurify from 'dompurify'
import escape from 'escape-html'
import Message from 'wf-dbd-react-ui/es/Message'
import ContentEventWrapper from 'wf-dbd-react-ui/es/ContentEventWrapper'
import { unescapeHtml } from 'wf-dbd-react-ui/es/lib'
import { requestNavigation } from 'wf-dbd-react-ui/es/actions'
import NavigationItemRecord from 'wf-dbd-react-ui/es/lib/records/NavigationItemRecord'
import ScrollToTopOnMount from 'wf-dbd-react-ui/es/ScrollToTopOnMount'
class MessageDisplayWithSpecialCharacters extends Component { //NOSONAR
constructor(props) {
super(props)
this.elementRef = null
}
componentDidMount() {
if (this.props.focusOnMount) {
if (this.elementRef) {
this.elementRef.blur() //needed to reset focus in iOS
this.elementRef.focus()
setTimeout(() => { this.elementRef.focus() }, 100) //timeout needed for Android
}
}
}
setElementRef = element => {
this.elementRef = element
}
handleClick = ({ target }) => {
const { requestNavigation } = this.props
if (target.hasAttribute('data-cui-link')) {
const navigationItem = new NavigationItemRecord({
samlNavigation: true,
displayType: 'saml',
navigationUrl: target.getAttribute('data-cui-link')
})
requestNavigation(navigationItem)
}
}
render() {
const { messages, className } = this.props
return (
<div className={className} tabIndex="-1" ref={this.setElementRef}>
<ScrollToTopOnMount />
{messages.map((message, index) => {
const purifiedContent = { __html: DOMPurify.sanitize(unescapeHtml(JSON.parse(`"${escape(window.decodeURIComponent(message.get('message')))}"`))) }
return (
<ContentEventWrapper handleContentClick={this.handleClick} key={index}>
<Message announce={true} level={message.get('level')}>
<p dangerouslySetInnerHTML={purifiedContent} />
</Message>
</ContentEventWrapper>
)
})}
</div>
)
}
}
MessageDisplayWithSpecialCharacters.propTypes = {
messages: PropTypes.array,
className: PropTypes.string,
focusOnMount: PropTypes.bool,
requestNavigation: PropTypes.func
}
const mapDispatchToProps = dispatch => ({
requestNavigation: navigationItem => dispatch(requestNavigation(navigationItem))
})
export default connect(null, mapDispatchToProps)(MessageDisplayWithSpecialCharacters)
Any help/ advice is appreciated for a novice like me.
Related
I am using next 13.1.0.
I have a ContextProvider that sets a light and dark theme
'use client';
import { Theme, ThemeContext } from '#store/theme';
import { ReactNode, useState, useEffect } from 'react';
interface ContextProviderProps {
children: ReactNode
}
const ContextProvider = ({ children }: ContextProviderProps) => {
const [theme, setTheme] = useState<Theme>('dark');
useEffect(() => {
const storedTheme = localStorage.getItem('theme');
if (storedTheme === 'light' || storedTheme === 'dark') {
setTheme(storedTheme);
} else {
localStorage.setItem('theme', theme);
}
// added to body because of overscroll-behavior
document.body.classList.add(theme);
return () => {
document.body.classList.remove(theme);
};
}, [theme]);
const toggle = () => {
const newTheme = theme === 'light' ? 'dark' : 'light';
setTheme(newTheme);
localStorage.setItem('theme', newTheme);
};
return (
<ThemeContext.Provider value={{ theme, toggle }}>
{children}
</ThemeContext.Provider>
);
};
export { ContextProvider };
I use it in my root layout
import '#styles/globals.scss';
import { GlobalContent } from '#components/GlobalContent/GlobalContent';
import { ContextProvider } from '#components/ContextProvider/ContextProvider';
import { Inter } from '#next/font/google';
import { ReactNode } from 'react';
const inter = Inter({ subsets: ['latin'] });
interface RootLayoutProps {
children: ReactNode
}
const RootLayout = ({ children }: RootLayoutProps) => {
return (
<html lang="en" className={inter.className}>
<head />
<body>
<ContextProvider>
<GlobalContent>
{children}
</GlobalContent>
</ContextProvider>
</body>
</html>
);
};
export default RootLayout;
And I consume the theme value in my GlobalContent
'use client';
import styles from '#components/GlobalContent/GlobalContent.module.scss';
import { GlobalHeader } from '#components/GlobalHeader/GlobalHeader';
import { GlobalFooter } from '#components/GlobalFooter/GlobalFooter';
import { ThemeContext } from '#store/theme';
import { ReactNode, useContext } from 'react';
interface GlobalContentProps {
children: ReactNode
}
const GlobalContent = ({ children }: GlobalContentProps) => {
const { theme } = useContext(ThemeContext);
return (
<div className={`${theme === 'light' ? styles.lightTheme : styles.darkTheme}`}>
<GlobalHeader />
<div className={styles.globalWrapper}>
<main className={styles.childrenWrapper}>
{children}
</main>
<GlobalFooter />
</div>
</div>
);
};
export { GlobalContent };
I get the error
Hydration failed because the initial UI does not match what was rendered on the server.
React docs error link
I don't understand why I am getting this error because I am accessing localStorage inside my useEffect, so I expect the HTML generated on the server to be the same with the client before the first render.
How can I solve this error?
I have made a workaround that solves the issue for now at the cost of giving up SSR.
By using a dynamic import on my ContextProvider, I disable server-rendering and the error is gone. As a bonus, the flashing issue from my default dark theme to my light theme saved on localStorage is gone. But I give up the benefits of SSR. If someone finds a better solution, please do share.
import '#styles/globals.scss';
import { GlobalContent } from '#components/GlobalContent/GlobalContent';
import { Inter } from '#next/font/google';
import dynamic from 'next/dynamic';
import { ReactNode } from 'react';
const inter = Inter({ subsets: ['latin'] });
interface RootLayoutProps {
children: ReactNode
}
// Fixes: Hydration failed because the initial UI does not match what was rendered on the server.
const DynamicContextProvider = dynamic(() => import('#components/ContextProvider/ContextProvider').then(mod => mod.ContextProvider), {
ssr: false
});
const RootLayout = ({ children }: RootLayoutProps) => {
return (
<html lang="en" className={inter.className}>
<head />
<body>
<DynamicContextProvider>
<GlobalContent>
{children}
</GlobalContent>
</DynamicContextProvider>
</body>
</html>
);
};
export default RootLayout;
This solution does not disable SSR site wide. I added a new test page with the following code
async function getData() {
const res = await fetch('https://rickandmortyapi.com/api/character', { cache: 'no-store' });
if (!res.ok) {
throw new Error('Failed to fetch data');
}
return res.json();
}
export default async function Page() {
const data = await getData();
return (
<main>
{data.results.map((c: any) => {
return (
<p key={c.id}>{c.name}</p>
);
})}
</main>
);
}
After running npm run build, I can see that the test page is using ssr
On checking the response for the test page, I can see the HTML response
I solved the error just by dynamically importing the default export of ContextProvider like this in _app.tsx . I'm also persisting context state in localStorage and it works without a problem .
_app.tsx
import dynamic from "next/dynamic";
const TodoProvider = dynamic(
() => import("#/util/context").then((ctx) => ctx.default),
{
ssr: false,
}
);
export default function MyApp({ Component, pageProps }: AppProps) {
return (
<TodoProvider>
<Component {...pageProps} />
</TodoProvider>
);
}
context.tsx
import React, {
useState,
FC,
createContext,
ReactNode,
useEffect,
} from "react";
export const TodoContext = createContext<TodoContextType | null>(null);
interface TodoProvider {
children: ReactNode;
}
const getInitialState = () => {
if (typeof window !== "undefined") {
const todos = localStorage.getItem("todos");
if (todos) {
return JSON.parse(todos);
} else {
return [];
}
}
};
const TodoProvider: FC<TodoProvider> = ({ children }) => {
const [todos, setTodos] = useState<ITodo[] | []>(getInitialState);
const saveTodo = (todo: ITodo) => {
const newTodo: ITodo = {
id: Math.random(),
title: todo.title,
description: todo.description,
status: false,
};
setTodos([...todos, newTodo]);
};
const updateTodo = (id: number) => {
todos.filter((todo: ITodo) => {
if (todo.id === id) {
todo.status = !todo.status;
setTodos([...todos]);
}
});
};
useEffect(() => {
if (typeof window !== "undefined") {
localStorage.setItem("todos", JSON.stringify(todos));
}
}, [todos]);
return (
<TodoContext.Provider value={{ todos, saveTodo, updateTodo }}>
{children}
</TodoContext.Provider>
);
};
export default TodoProvider;
I am building an app with react / redux for managing Collection of Electronic equipment (=donations). I have several routes that their functionality - is similiar - fetching entity (it could be volunteer, donor etc) data and show it in a table.
the volunteer route:
import React, {Component} from 'react';
import { connect } from 'react-redux';
import { requestVolunteerData } from '../actions/entitiesAction';
import { volenteerColumns as columns } from '../utils/entitiesColumns/volenteerColumns';
import '../container/App.css';
import Table from '../components/Table/Table';
import Loading from '../components/Loading/Loading';
const mapStateToProps = state => {
return {
entities: state.requestEntitiesReducer.entities,
isPending: state.requestEntitiesReducer.isPending,
error: state.requestEntitiesReducer.error
}
}
const mapDispatchToProps = dispatch => {
return {
onRequestEntities: () => dispatch(requestVolunteerData())
}
}
class Volenteer extends Component{
componentDidMount () {
this.props.onRequestEntities();
}
render () {
const { entities, isPending} = this.props;
return isPending ?
<Loading />
:
(
<div className='tc'>
<h1 className='f2'>רשימת מתנדבים</h1>
<Table data={ entities } columns={ columns } />
</div>
);
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Volenteer);
and a consumer route look like this:
import React, {Component} from 'react';
import { connect } from 'react-redux';
import { requestConsumerData } from '../actions/entitiesAction';
import { consumerColumns as columns } from '../utils/entitiesColumns/consumerColumns';
import '../container/App.css';
import Table from '../components/Table/Table';
import Loading from '../components/Loading/Loading';
const mapStateToProps = state => {
return {
entities: state.requestEntitiesReducer.entities,
isPending: state.requestEntitiesReducer.isPending,
error: state.requestEntitiesReducer.error
}
}
const mapDispatchToProps = dispatch => {
return {
onRequestEntities: () => dispatch(requestConsumerData())
}
}
class Consumer extends Component{
componentDidMount () {
this.props.onRequestEntities();
}
render () {
const { entities, isPending} = this.props;
return isPending ?
<Loading />
:
(
<div className='tc'>
<h1 className='f2'>רשימת נזקקים</h1>
<Table data={ entities } columns={ columns }/>
</div>
);
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Consumer);
As you can see, they both have the same logic and the differences are:
the action
the Entity name for the h1 tag
the columns object
the data of course
so I tried to implement an HOC which look like this:
import React, {Component} from 'react';
import { connect } from 'react-redux';
import '../container/App.css';
import Table from '../Table/Table';
import Loading from '../Loading/Loading';
export default function WithEntity (EntityComponent, action, columns, name) {
const mapStateToProps = state => {
return {
isPending: state.requestEntitiesReducer.isPending,
entities: state.requestEntitiesReducer.entities,
error: state.requestEntitiesReducer.error
}
}
const mapDispatchToProps = dispatch => {
return {
onRequestEntities: () => dispatch(action)
}
}
class extends Component {
componentDidMount () {
this.props.onRequestEntities();
}
render() {
return (
<EntityComponent {...this.props} />
)
}
}
return connect(mapStateToProps, mapDispatchToProps)(EntityComponent);
}
and the volunteer should look like:
const volunteerHoc = WithEntity (volunteer, action, columns, name);
const consumerHoc = WithEntity (consumer, action, columns, name)
but I did not understand how to inject the Loading and Table components, and wht the name of the class inside the HOC should be-
should I use another HOC - something like WithLoader that receive the data from the first one and render the Loading and Table components with the proper data? just to mention that connect is HOC itself so I need to return the EntityComponent to the redux store :
return connect(mapStateToProps, mapDispatchToProps)(EntityComponent);
I Would appreciate any help
OK, I made it, the HOC takes a basic component, Expands the functionality (by adding methods and managing state for ex) and return a new (henanced) comp with this props.
lets create a simple volunteer comp:
import React, {Component} from 'react';
import { requestVolunteerData } from '../actions/entitiesAction';
import { volenteerColumns as columns } from '../utils/entitiesColumns/volenteerColumns';
import '../container/App.css';
import WithEntity from '../components/HOC/WithEntity.jsx';
import Table from '../components/Table/Table';
import Loading from '../components/Loading/Loading';
class Volenteer extends Component {
render() {
const { entities, isPending} = this.props;
return isPending ?
<Loading />
:
(
<div className='tc'>
<h1 className='f2'>רשימת מתנדבים</h1>
<Table data={ entities } columns={ columns } />
</div>
);
}
}
const VolenteerHOC = WithEntity(Volenteer, requestVolunteerData() );
export default VolenteerHOC;
now lets create the HOC WithEntity that managing the state and return the new cmop to redux state by connect:
import React, {Component} from 'react';
import { connect } from 'react-redux';
const WithEntity = (EntityComponent, action) => {
const mapStateToProps = state => {
return {
isPending: state.requestEntitiesReducer.isPending,
entities: state.requestEntitiesReducer.entities,
error: state.requestEntitiesReducer.error
}
}
const mapDispatchToProps = dispatch => {
return {
onRequestEntities: () => dispatch(action)
}
}
class NewCmoponent extends Component {
componentDidMount () {
this.props.onRequestEntities();
}
render() {
const { entities, isPending} = this.props;
return (
<EntityComponent {...this.props} />
)
}
}
return connect(mapStateToProps, mapDispatchToProps)(NewCmoponent );
}
export default WithEntity;
Now same route can be simply generated via this HOC.
check out this video:
https://www.youtube.com/watch?v=rsBQj6X7UK8
Please tell me why, when I call this.props.getOnEvents(), an error occurs that “getOnEvents() is not a function”, what’s wrong here and how to fix it?
I’m looking at the console.log and there is this function in the props, but when I try to call, an error flies out that it’s not a function
EventCalendar.js
import React, { Component } from 'react';
import { withStyles } from '#material-ui/core/styles';
import EventCalendarTable from './EventCalendarTable';
import EventCalendarView from './EventCalendarView';
const styles = theme => ({
root: {
width: '80%',
margin: '20px auto 0'
},
});
class EventCalendar extends Component {
constructor(props) {
super(props);
this.state = {
viewEvents: 'table'
};
}
componentDidMount() {
this.props.onGetEvents();
}
changeEventsView = (value) => {
this.setState({ viewEvents: value });
}
render() {
console.log(this.props);
const { classes } = this.props;
return (
<div className={classes.root}>
<EventCalendarView changeEventsView={this.changeEventsView}/>
{
this.state.viewEvents === 'table'
? <EventCalendarTable />
: <div>test</div>
}
</div>
);
}
}
export default withStyles(styles)(EventCalendar);
EventPage/component.jsx
import React from 'react';
import EventCalendar from '../../components/EventCalendar';
import './index.scss';
function Events(props) {
return (
<React.Fragment>
<EventCalendar props={props}/>
</React.Fragment>
);
}
export default Events;
EventPage/container.js
import { connect } from 'react-redux';
import EventsPage from './component';
import { getEvents } from '../../../store/modules/Events/operations';
const mapStateToProps = ({ Events }) => ({
Events
});
const mapDispatchToProps = dispatch => ({
onGetEvents: () => {
console.log(123);
dispatch(getEvents());
}
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(EventsPage);
Events/actions.js
import * as types from './types';
export const eventsFetch = value => ({
type: types.FETCHING_EVENTS,
payload: value
});
export const setEvents = ({ objById, arrayIds }) => ({
type: types.SET_EVENTS,
payload: {
eventById: objById,
eventsOrder: arrayIds
}
});
Events/types.js
export const FETCHING_EVENTS = 'Events/FETCHING_EVENTS';
export const SET_EVENTS = 'Events/SET_EVENTS';
Events/operation.js
import FetchClient from 'app/utils/FetchClient';
import IdsAndByIds from 'app/utils/IdsAndByIds';
import { eventsFetch, setEvents } from './actions';
export const getEvents = () => async (dispatch) => {
try {
const { data } = await FetchClient.get('/events');
dispatch(setEvents(IdsAndByIds(data)));
dispatch(eventsFetch(false));
} catch (error) {
console.log(error);
}
};
Events/reducer.js
import { createReducer } from 'store/utils';
import * as types from './types';
const usersInitState = {
fetching: true,
events: {
eventById: null,
usersOrder: null
},
error: null
};
const eventsReducer = createReducer(usersInitState)({
[types.FETCHING_EVENTS]: (state, { payload }) => ({
...state,
fetching: payload
}),
[types.SET_EVENTS]: (state, { payload }) => ({
...state,
events: {
...payload
}
})
});
export default eventsReducer;
Events/index.js
import { combineReducers } from 'redux';
import * as eventsListOperations from './operations';
import reducer from './reducers';
const EventsReducer = combineReducers({
eventsList: reducer
});
export default EventsReducer;
export { eventsListOperations };
The issue here is a minor one, Since you are connecting Events component to connect, you are receiveing the prop onGetEvents in that component, Now inside this component you are passing the props by a name props to the EventCalendar component
<EventCalendar props={props}/>
Now the props in EventCalender will contain a key called as props which wil lhave your data but you are trying to access it directly on props which is why it is undefined.
The correct way to pass the props here would be to use spread syntax like
<EventCalendar {...props}/>
I want to push state to the browser and append to the pathname when a subreddit has changed.
In the example below the user chooses an option from ['reactjs', 'frontend']. So when the user chooses reactjs, I want to changethe browser url to: <url>/reddit/reactjs or <url>/reddit/frontend based on the selection.
So when the user goes back and forward, I want to show data that was already fetched.
How can I make it work with react-redux for the example below? Normally, I was using history.pushState(...).
Note: I am using connected-react-router
index.js:
import 'babel-polyfill'
import React from 'react'
import { render } from 'react-dom'
import Root from './containers/Root'
render(<Root />, document.getElementById('root'))
action.js:
import fetch from 'cross-fetch'
export const REQUEST_POSTS = 'REQUEST_POSTS'
export const RECEIVE_POSTS = 'RECEIVE_POSTS'
export const SELECT_SUBREDDIT = 'SELECT_SUBREDDIT'
export const INVALIDATE_SUBREDDIT = 'INVALIDATE_SUBREDDIT'
export function selectSubreddit(subreddit) {
return {
type: SELECT_SUBREDDIT,
subreddit
}
}
export function invalidateSubreddit(subreddit) {
return {
type: INVALIDATE_SUBREDDIT,
subreddit
}
}
function requestPosts(subreddit) {
return {
type: REQUEST_POSTS,
subreddit
}
}
function receivePosts(subreddit, json) {
return {
type: RECEIVE_POSTS,
subreddit,
posts: json.data.children.map(child => child.data),
receivedAt: Date.now()
}
}
function fetchPosts(subreddit) {
return dispatch => {
dispatch(requestPosts(subreddit))
return fetch(`https://www.reddit.com/r/${subreddit}.json`)
.then(response => response.json())
.then(json => dispatch(receivePosts(subreddit, json)))
}
}
function shouldFetchPosts(state, subreddit) {
const posts = state.postsBySubreddit[subreddit]
if (!posts) {
return true
} else if (posts.isFetching) {
return false
} else {
return posts.didInvalidate
}
}
export function fetchPostsIfNeeded(subreddit) {
return (dispatch, getState) => {
if (shouldFetchPosts(getState(), subreddit)) {
return dispatch(fetchPosts(subreddit))
}
}
}
reducers.js:
import { combineReducers } from 'redux'
import {
SELECT_SUBREDDIT,
INVALIDATE_SUBREDDIT,
REQUEST_POSTS,
RECEIVE_POSTS
} from './actions'
function selectedSubreddit(state = 'reactjs', action) {
switch (action.type) {
case SELECT_SUBREDDIT:
return action.subreddit
default:
return state
}
}
function posts(
state = {
isFetching: false,
didInvalidate: false,
items: []
},
action
) {
switch (action.type) {
case INVALIDATE_SUBREDDIT:
return Object.assign({}, state, {
didInvalidate: true
})
case REQUEST_POSTS:
return Object.assign({}, state, {
isFetching: true,
didInvalidate: false
})
case RECEIVE_POSTS:
return Object.assign({}, state, {
isFetching: false,
didInvalidate: false,
items: action.posts,
lastUpdated: action.receivedAt
})
default:
return state
}
}
function postsBySubreddit(state = {}, action) {
switch (action.type) {
case INVALIDATE_SUBREDDIT:
case RECEIVE_POSTS:
case REQUEST_POSTS:
return Object.assign({}, state, {
[action.subreddit]: posts(state[action.subreddit], action)
})
default:
return state
}
}
const rootReducer = combineReducers({
postsBySubreddit,
selectedSubreddit
})
export default rootReducer
configureStore.js
import { createStore, compose, applyMiddleware } from 'redux'
import { createBrowserHistory } from 'history'
import { routerMiddleware } from 'connected-react-router'
import thunkMiddleware from 'redux-thunk'
import logger from 'redux-logger'
import rootReducer from '../reducers'
// const loggerMiddleware = createLogger()
export const history = createBrowserHistory()
export default function configureStore(preloadedState?: any) {
const store = createStore(
rootReducer(history), // root reducer with router state
preloadedState,
compose(
applyMiddleware(
thunkMiddleware,
logger,
routerMiddleware(history), // for dispatching history actions
// ... other middlewares ...
),
),
)
return store
}
Root.js
import React, { Component } from 'react'
import { Provider } from 'react-redux'
import configureStore from '../configureStore'
import AsyncApp from './AsyncApp'
const store = configureStore()
export default class Root extends Component {
render() {
return (
<Provider store={store}>
<AsyncApp />
</Provider>
)
}
}
AsnycApp.js
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import {
selectSubreddit,
fetchPostsIfNeeded,
invalidateSubreddit
} from '../actions'
import Picker from '../components/Picker'
import Posts from '../components/Posts'
class AsyncApp extends Component {
constructor(props) {
super(props)
this.handleChange = this.handleChange.bind(this)
this.handleRefreshClick = this.handleRefreshClick.bind(this)
}
componentDidMount() {
const { dispatch, selectedSubreddit } = this.props
dispatch(fetchPostsIfNeeded(selectedSubreddit))
}
componentDidUpdate(prevProps) {
if (this.props.selectedSubreddit !== prevProps.selectedSubreddit) {
const { dispatch, selectedSubreddit } = this.props
dispatch(fetchPostsIfNeeded(selectedSubreddit))
}
}
handleChange(nextSubreddit) {
this.props.dispatch(selectSubreddit(nextSubreddit))
this.props.dispatch(fetchPostsIfNeeded(nextSubreddit))
}
handleRefreshClick(e) {
e.preventDefault()
const { dispatch, selectedSubreddit } = this.props
dispatch(invalidateSubreddit(selectedSubreddit))
dispatch(fetchPostsIfNeeded(selectedSubreddit))
}
render() {
const { selectedSubreddit, posts, isFetching, lastUpdated } = this.props
return (
<div>
<Picker
value={selectedSubreddit}
onChange={this.handleChange}
options={['reactjs', 'frontend']}
/>
<p>
{lastUpdated && (
<span>
Last updated at {new Date(lastUpdated).toLocaleTimeString()}.{' '}
</span>
)}
{!isFetching && (
<button onClick={this.handleRefreshClick}>Refresh</button>
)}
</p>
{isFetching && posts.length === 0 && <h2>Loading...</h2>}
{!isFetching && posts.length === 0 && <h2>Empty.</h2>}
{posts.length > 0 && (
<div style={{ opacity: isFetching ? 0.5 : 1 }}>
<Posts posts={posts} />
</div>
)}
</div>
)
}
}
AsyncApp.propTypes = {
selectedSubreddit: PropTypes.string.isRequired,
posts: PropTypes.array.isRequired,
isFetching: PropTypes.bool.isRequired,
lastUpdated: PropTypes.number,
dispatch: PropTypes.func.isRequired
}
function mapStateToProps(state) {
const { selectedSubreddit, postsBySubreddit } = state
const { isFetching, lastUpdated, items: posts } = postsBySubreddit[
selectedSubreddit
] || {
isFetching: true,
items: []
}
return {
selectedSubreddit,
posts,
isFetching,
lastUpdated
}
}
export default connect(mapStateToProps)(AsyncApp)
Picker.js
import React, { Component } from 'react'
import PropTypes from 'prop-types'
export default class Picker extends Component {
render() {
const { value, onChange, options } = this.props
return (
<span>
<h1>{value}</h1>
<select onChange={e => onChange(e.target.value)} value={value}>
{options.map(option => (
<option value={option} key={option}>
{option}
</option>
))}
</select>
</span>
)
}
}
Picker.propTypes = {
options: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired,
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired
}
Posts.js:
import React, { Component } from 'react'
import PropTypes from 'prop-types'
export default class Posts extends Component {
render() {
return (
<ul>
{this.props.posts.map((post, i) => (
<li key={i}>{post.title}</li>
))}
</ul>
)
}
}
Posts.propTypes = {
posts: PropTypes.array.isRequired
}
Update:
import { push } from 'connected-react-router';
...
handleChange(nextSubreddit) {
this.props.dispatch(push('/reddit/' + nextSubreddit))
}
I placed this in the handleChange() method. When Picker changes, I push the state to the browser. However, when I go back and forward, the data does not change according to this url. I see the same data in every state.
We can handle this scenario using history property. We implement using listener of history and play with the location property which in turn provide pathname. It would be implement in componentDidUpdate. Everytime when back and forward button of browser clicked, the listener will called and service calls and state can be changed accordingly.
AsyncApp.js
// code here
import { history } from '../configureStore'
// code here
componentDidUpdate(prevProps) {
if (this.props.selectedSubreddit !== prevProps.selectedSubreddit) {
const backBrowser = history.listen(location => {
console.log(location.pathname)
// code here
}
// code here
}
}
I've asked a similar-ish question here before, however my code has changed quite a bit and I can not figure this out. I am certain it's an issue with what I am passing to my action/reducer. I would seriously appreciate it if someone could explain what I am doing wrong here. I really want to get this, just having a hard time with it.
actions.js
import { ADD_TODO, REMOVE_TODO } from '../constants/action-types';
export const addTodo = (todo) => (
{
type: ADD_TODO,
payload: todo
}
);
export const removeTodo = (id) => (
{
type: REMOVE_TODO,
payload: id
}
)
reducers.js
import { ADD_TODO, REMOVE_TODO, ADD_OPTIONS } from '../constants/action-types';
import uuidv1 from 'uuid';
const initialState = {
todos: []
};
const rootReducer = (state = initialState, action) => {
switch (action.type) {
case ADD_TODO:
return {
...state,
todos: [...state.todos,
{
title: action.payload.inputValue,
id: uuidv1(),
createdAt: Date(),
priority: '',
deadline: '',
isClicked: false
}]
}
case REMOVE_TODO:
return {
...state,
todos: [...state.todos.filter(todo => todo.id !== action.payload)]
}
case ADD_OPTIONS:
return {
...state,
todos: [...state.todos,
{
isClicked: false
}]
}
default:
return state;
}
}
export default rootReducer;
TodoList.js
import React, { Component } from 'react';
import TodoItem from './TodoItem';
import { removeTodo } from '../actions';
import { connect } from 'react-redux';
const mapDispatchToProps = dispatch => {
return {
removeTodo: id => dispatch(removeTodo(id))
};
};
const mapStateToProps = state => {
return {todos: [...state.todos]};
};
class List extends Component {
render() {
const mappedTodos = this.props.todos.map((todo, index) => (
<TodoItem
title={todo.title}
key={index}
removeTodo={this.props.removeTodo}
/>
));
return (
mappedTodos
);
}
}
const TodoList = connect(mapStateToProps, mapDispatchToProps) (List)
export default TodoList;
TodoItem.js
import React, { Component } from 'react';
import uuid from 'uuid';
import '../../css/Todo.css';
class TodoItem extends Component {
render() {
const todoId = uuid();
return (
<div id={todoId}>
{this.props.title}
<button onClick={this.props.removeTodo}>X</button>
</div>
);
}
}
export default TodoItem;
You need to wrap your remove handler in an expression that can be evaluated at "click time" and use the todo id from the closure:
class TodoItem extends Component {
render() {
const todoId = uuid();
return (
<div id={todoId}>
{this.props.title}
<button onClick={this.props.removeTodo}>X</button>
</div>
);
}
}
Should be something like...
class TodoItem extends Component {
render() {
const todoId = uuid();
return (
<div id={todoId}>
{this.props.title}
<button onClick={() => this.props.removeTodo(todoId)}>X</button>
</div>
);
}
}
Along the lines of what #The Dembinski was saying, it works when I change my TodoList component to look like this:
import React, { Component } from 'react';
import TodoItem from './TodoItem';
import { removeTodo } from '../actions';
import { connect } from 'react-redux';
const mapDispatchToProps = dispatch => {
return {
removeTodo: id => dispatch(removeTodo(id))
};
};
const mapStateToProps = state => {
return {todos: [...state.todos]};
};
class List extends Component {
render() {
const mappedTodos = this.props.todos.map((todo, index) => (
<TodoItem
title={todo.title}
key={index}
removeTodo={() => this.props.removeTodo(todo.id)}
/>
));
return (
mappedTodos
);
}
}
const TodoList = connect(mapStateToProps, mapDispatchToProps) (List)
export default TodoList;
Changing my removeTodo prop in the map here DID fix the issue and now deletes properly. However, if anyone could help me understand this better either by further discussion, or just by pointing my in the right direction as to what I should be researching. Would be greatly appreciated. I'm not after answers, I'm after learning.