I'm modernising a react application that I'm working on and I started to use hooks.
Before the component I had was the classic class component, with the constructor, state and everything. I was hooking it to redux the following way:
import React from 'react';
import { createStore, combineReducers } from 'redux';
import { Provider, connect } from 'react-redux';
import Logs from "NS/src/Screen/Logs"
import store from 'NS/src/Reducers/store'
const LogsStore = connect(state => ({ store: state.store }))(Logs);
Now I'm following a tutorial on Hooks, so I change my component accordingly:
export default function Logs() {
const {status, filteredStatus} = this.props.store
const [filter, setFilter] = React.useState('')
clearLogs = () => {
...
}
...
}
the problem is that now this is undefined. How can I access the store from this kind of component?
this is undefined because functional components do not have an instance, the redux props are available at props which is a regular object
const mapState = state => ({ foo : state.foo })
const Component = connect(mapState)(props =>{
console.log(props.foo)
})
Props are passed as an argument to your functional component:
export default function Logs(props) {
const {status, filteredStatus} = props.store
const [filter, setFilter] = React.useState('')
clearLogs = () => {
...
}
...
}
...and if you're into saving lines:
export default function Logs({ store: { status, filteredStatus } }) {
const [filter, setFilter] = React.useState('')
clearLogs = () => {
...
}
...
}
Related
trying to share the states from one component to another: The state can be accessed from main component but it comes undefined when accessing from a new component
This is my reducer:
export const tableReducer=(state = [], action)=> {
switch (action.type) {
case 'SELECTED_LIST':
state = JSON.parse(JSON.stringify(action.payload));
return state;
default:
return state
}
}
access it from a different file:
const [userList, usersDispatch] = useReducer(tableReducer, []);
useEffect(() => {
const list = Object.keys(selectedRowIds).length > 0 ? selectedFlatRows.map(
d => d.original.email
)
: '';
usersDispatch({ type: 'SELECTED_LIST', payload: list, });
}, [selectedRowIds, selectedFlatRows]);
and in a new component:
const [userList] = useReducer(tableReducer);
const deleteUsers = () => {
console.log(userList)
}
but here console.log(userList) it results to undefined
For Sharing of state between components, you can use Context API with useReducer.
Context API provides a neat way of providing state to child components without ending up with a prop drilling situation. It requires that a Provider is setup, which provides its values to any of its Consumers. Any component that is a child of the Provider can consume the context.
First a piece of context is created.
CustomContext.js
import React from 'react';
const CustomContext = React.createContext();
export function useCustomContext() {
return React.useContext(CustomContext);
}
export default CustomContext;
We can define your reducer in a seperate file.
TableReducer.js
export const tableReducer=(state = [], action)=> {
switch (action.type) {
case 'SELECTED_LIST':
state = JSON.parse(JSON.stringify(action.payload));
return state;
default:
return state
}
}
next is to implement the provider, and give it a value within a "Parent" component (A higher up component)
Parent.js
import CustomContext from './CustomContext'
import { tableReducer } from './TableReducer'
const ParentComponent = () => {
const [userState, usersDispatch ] = React.useReducer(tableReducer, []);
const providerState = {
userState,
usersDispatch
}
return (
<CustomContext.Provider value={providerState} >
<ChildComponent /> //Any component within here can access value by using useCustomContext();
</CustomContext.Provider>
)
}
now any component nested within <CustomContext.Provider></CustomContext.Provider> can access whatever is passed into "value" prop of the Provider which is your context state and the dispatch method.
The child component will look like this (I have ommited your state values and such..)
Child.js
import { useCustomContext }from './CustomContext'
const ChildComponent = (props) => {
//your custom state variables and other methods
const { userState, usersDispatch } = useCustomContext();
useEffect(() => {
const list = Object.keys(selectedRowIds).length > 0 ? selectedFlatRows.map(
d => d.original.email
)
: '';
usersDispatch({ type: 'SELECTED_LIST', payload: list, });
}, [selectedRowIds, selectedFlatRows]);
return(
<div>your components dependent on selectedRowIds, selectedFlatRows<div>
)
}
You can't share the state with useReducer hook like you are trying to. Each call to useReducer returns a new state that is managed using the reducer function passed to useReducer hook.
Just as each call to useState returns a different state, each call to useReducer returns a different state. Two useReducer calls can't share the same state.
To share the state, you can use one of the following options:
Context API
React-Redux
Pass the state from parent component to child component using props
#Gandzal is correct but I found it was lacking a typscript version and also today createContext requieres a default parameter. This came up as one of the top answers on google so I thought I would share.
I setup my solution like this:
Custom context:
import React, {Dispatch} from 'react';
import {StateType, Action} from './reducer'
interface IContextProps {
state: StateType;
dispatch:Dispatch<Action>
}
const CustomContext = React.createContext({} as IContextProps);
export function useCustomContext() {
return React.useContext(CustomContext);
}
export default CustomContext;
Note StateType and Action:
export type StateType = {
items: Array<DataItems>;
date: Date;
};
export type Action = {
type: ActionKind;
payload: DataItems;
};
reducer:
export const reducer = (state: StateType, action: Action) => {
const { type, payload } = action;
let newArray: Array<DataItems> = [];
switch (type) {
case ActionKind.Checked:
newArray = state.items.map((item) => ({
...item,
checked: item.id === payload.id ? true : item.checked,
}));
return {
...state,
items: newArray,
}
default:
return state;
}
};
App.tsx:
import { reducer, initalState } from 'Shared/Reducer/reducer';
import CustomContext from 'Shared/Reducer/CustomContext';
const App: React.FC = () => {
const [state, dispatch] = React.useReducer(reducer, initalState);
const providerState = {
state,
dispatch,
};
return (
<CustomContext.Provider value={providerState}>
<main role="main">
// your components
</main>
</CustomContext.Provider>
);
};
export default App;
And one of your components:
import { useCustomContext } from 'Shared/Reducer/CustomContext';
export const MyComp: React.FC<MyType> = (props) => {
const { data} = props;
const { state, dispatch } = useCustomContext(); --- Your state and dispatch here
return (
<div>
// your component
</div >
);
}
While I am trying to test an Redux connected react component, I am unable to provide .Below is the code for my connected react component :
MyComponent.jsx
class MyComponent extends React.Component{
componentWillMount (){
}
componentDidMount () {
setTimeOut (()=>{
changeValue('personName' , 'something',someVar)
},2000);
}
}
export default MyComponent
index.jsx
import MyComponent from './MyComponent'
const mapStateToProps = state => (
{
rootState : state
info : state.someInfo
}
)
const mapDispatchProps = dispatch => bindActionCreators({
changeValue : change,
resetValue : reset
},dispatch)
export default compose (connect (mapStateToProps,mapDispatchProps),injector,theme.wizard({}))(
connect(state => ({
values : selector (state, 'vehicleName', 'address','personName')
}))
)(MyComponent)
MyComponent.Test.js
import configureStore from 'redux-mock-store'
import {shallow, mount} from 'enzyme'
import MyComponent from './MyComponent'
jest.useFakeTimers();
describe('test1', ()=>{
it('test for componentDidMount' , ()=>{
const middlewares = []
const mockStore = configureStore(middlewares)
const initialState = {changeValue : (x,y,z)=> console.log(x)};
const store = mockStore(initialState);
var component = shallow (<Provider store = {store}><MyComponent /></Provider>);
component.update();
jest.runAllTimers();
})
}
When I run this I get error : changeValue is not a function , How can I pass/Mock changeValue function in the test to resolve this error.
When using useContext() I'm getting an empty object.
I created a file that contains my Context, Provider, and Consumer
src/utils/notesContext.js
import PropTypes from "prop-types"
import React, { createContext, useState } from "react"
export const Context = createContext({})
export const Provider = props => {
const {
notes: initialNotes,
selectedNote: initialSelectedNotes,
children,
} = props
const [notes, setNotes] = useState([[""]])
const [selectedNote, setSelectedNote] = useState([[""]])
const addNewNote = note => {
console.log(note)
}
const notesContext = {
notes,
setNotes,
selectedNote,
setSelectedNote,
addNewNote,
}
return <Context.Provider value={notesContext}>{children}</Context.Provider>
}
export const { Consumer } = Context
Provider.propTypes = {
notes: PropTypes.array,
selectedNote: PropTypes.object,
}
Provider.defaultProps = {
notes: [],
selectedNote: {},
}
Then within my index file, I have the following
src/pages/index.js
import React, { useContext } from "react"
import { Context } from "../utils/notesContext"
const Index = ({ data, location }) => {
const initNote = data.note
const notesContext = useContext(Context) // notesContext is empty
const { notes, selectedNote, setSelectedNote, addNewNote } = notesContext
addNewNote(initNote)
...
}
Did I set up something incorrectly with my context or provider? Another thing worth mentioning is that this is in a Gatsby application so not sure if this could be causing an underlying issue that I'm not aware of.
Thanks in advance!
Your Index component should be used inside notesContext Provider.
Read more about Context.Provider.
import { Context, Provider } from "./notesContext";
const Index = () => {
const notesContext = useContext(Context);
console.log(notesContext); // now this is not an empty object
return <p>Index</p>;
};
export default function App() {
// notice here, we're wrapping Index inside our Provider
return (
<Provider>
<Index />
</Provider>
);
}
I have the following custom hook called useFlash:
import { useState } from 'react';
export default function useFlash() {
const [messages, setMessages] = useState([]);
const showFlash = (message: string) => {
setMessages([...messages, message]);
};
const clearMessage = (index: number) => {
setMessages(messages.filter((_m, i) => index !== i));
};
return {
messages,
showFlash,
clearMessage
};
}
Then I have this HOC providing it to two other components:
import React from 'react';
import useFlash from '../effects/useFlash';
const withFlash = (WrappedComponent: React.Component) => {
const WithFlash = () => {
const { messages, showFlash, clearMessage } = useFlash();
return (
<WrappedComponent
messages={messages}
showFlash={showFlash}
clearFlashMessage={clearMessage}
/>
);
};
return WithFlash;
};
export default withFlash;
It works well, except each use of the HOC gets its own state data. I need the state to be global. I know I can use contexts with consumer/providers, but I thought this way would be a little simpler. It is not proving to be true, is there a way to make this global?
You'll need to use Context, but it's not that bad..
create your context..
import React, { useState } from 'react';
export const FlashContext = React.createContext();
export const FlashProvider = ({ children }) => {
const [messages, setMessages] = useState([]);
return (
<FlashContext.Provider value={{ messages, setMessages }}>
{children}
</FlashContext.Provider>
);
};
wrap your components in the provider somewhere higher in the tree..
import React from "react";
import { FlashProvider } from "./flash-context";
const App = () => <FlashProvider><TheRest /></FlashProvider>;
export default App;
then use the context in your custom hook..
import React, { useContext } from "react";
import { FlashContext } from "./flash-context";
export default function useFlash() {
const { messages, setMessages } = useContext(FlashContext);
const showFlash = (message) => {
setMessages([...messages, message]);
};
const clearMessage = (index) => {
setMessages(messages.filter((_m, i) => index !== i));
};
return {
messages,
showFlash,
clearMessage
};
}
So I would like to test mapStateToProps and mapDispatchToProps with Enzyme/Jest.
I have a component DrawerAvatar like this:
DrawerAvatar.js
const mapStateToProps = state => ({
isAuthenticated: state.authReducer.isAuthenticated
});
export default compose(
connect(mapStateToProps, null)
)(DrawerAvatar);
DrawerAvatar.test.js
import configureMockStore from 'redux-mock-store';
import connectedDrawerAvatar, { DrawerAvatar } from './DrawerAvatar';
const mockStore = configureMockStore();
it('mapStateToProps should return the right value', () => {
const initialState = {
someState: 123
};
const store = mockStore(initialState);
const wrapper = shallow(<connectedDrawerAvatar store={store} />);
expect(wrapper.props().someState).toBe(123);
});
However, this doesn't work because wrapper.props().someState returns undefined... So I have no clue how to test mapStatesToProps along with redux-mock-store using the connected component.
I don't know neither how to test mapDispatchToProps ..!
I've tried the methods providing in this blog but it doesn't work.
Thank you very much !
EDIT:
This works, but I'm not sure if it really tests the mapStateToProps... Can someone confirm that this is the right way to test mapStateToProps ?
DrawerAvatar.test.js
it('mapStateToProps should return the right value', () => {
const initialState = {
isAuthenticated: false
};
const mockStore = configureMockStore();
const store = mockStore(initialState);
const wrapper = shallow(<connectedDrawerAvatar store={store} />);
expect(wrapper.props().store.getState().isAuthenticated).toBe(false);
});
One way I found from : redux discussion on github is
import React from 'react';
import { shallow } from 'enzyme';
import configureMockStore from 'redux-mock-store';
import ConnectedDrawerAvatar from './DrawerAvatar';
describe('DrawerAvatar', ()=> {
const mockStore = configureMockStore();
it.each([true, false], 'receives correct value from store as props', (value)=> {
const initialState = { authReducer: { isAuthenticated: value } }
const store = mockStore(initialState)
const wrapper = shallow(<ConnectedDrawerAvatar store={store} />)
expect(wrapper.props().isAuthenticated).toBe(value)
})
})
You can also try this instead :
In my opinion Testing mapStateToProps(), you need to identify the props for the particular state. Also used provider which makes the components available that are wrapped in Connect() function.
import React from 'react';
import { shallow } from 'enzyme';
import { Provider } from 'react-redux';
import configureMockStore from 'redux-mock-store';
import ConnectedDrawerAvatar from './DrawerAvatar';
describe('DrawerAvatar', ()=> {
let component;
let store;
let value;
beforeEach(() => {
const initialState = {
authReducer: { isAuthenticated: value }
};
store = mockStore(initialState);
component = shallow(<Provider store={store}><ConnectedDrawerAvatar/></Provider>);
});
it('should return exact value from the store(props)', () => {
expect(component.props().isAuthenticated).toBe(value);
});
});
Hope this helps!
Easiest way to test that is to that mapStateToProps directly, like this:
export const mapStateToProps = state => ({
isAuthenticated: state.authReducer.isAuthenticated
});
and have a test like this:
it('mapStateToProps should return the right value', () => {
const mockedState = {
authReducer: {
isAuthenticated: false
}
};
const state = mapStateToProps(mockedState);
expect(state).toBeFalsy();
});
But I dont really see why you should do that.
IMO you should not test connected components. Just export the container class and test it directly.
The reason why one should not test component connection is that it is tested well in the library itself so by testing that you don't really add any value.