I want to use my Context in a class component (MyEventsScreen), but whenever I do so, I get an error saying it is an invalid hooks call.
Here is MyEventsScreen.js (I've simplified it to be brief):
import { useAuthState } from "../contexts/authContext";
export default class MyEventsScreen extends Component {
render() {
return (
<View>
<Text></Text>
</View>
}
I have my authContext.js here:
import React from "react";
import { authReducer } from "../reducers/authReducer";
const AuthStateContext = React.createContext();
const AuthDispatchContext = React.createContext();
function AuthProvider({ children }) {
const [state, dispatch] = React.useReducer(authReducer, {
isLoading: true,
isSignout: false,
userToken: null,
email: null,
userID: null,
});
return (
<AuthStateContext.Provider value={state}>
<AuthDispatchContext.Provider value={dispatch}>
{children}
</AuthDispatchContext.Provider>
</AuthStateContext.Provider>
);
}
function useAuthState() {
const context = React.useContext(AuthStateContext);
if (context === undefined) {
throw new Error("useAuthState must be used within a AuthProvider");
}
return context;
}
function useAuthDispatch() {
const context = React.useContext(AuthDispatchContext);
if (context === undefined) {
throw new Error("useAuthDispatch must be used within a AuthProvider");
}
return context;
}
export { AuthProvider, useAuthState, useAuthDispatch };
I have an authReducer.js here:
export const authReducer = (prevState, action) => {
switch (action.type) {
case "SIGN_IN":
return {
...prevState,
email: action.email,
isSignout: false,
userToken: action.token,
};
default:
return prevState;
}
};
Any help would be appreciated!
For class components you can use the Consumer provided by the context .
export your AuthStateContext
export const AuthStateContext = React.createContext();
In the class component you can use it as
import { useAuthState, AuthStateContext } from "../contexts/authContext";
export default class MyEventsScreen extends Component {
render() {
return (
<AuthStateContext.Consumer>
{(value) => { // read the context value here
return (
<View>
<Text></Text>
</View>
);
}}
</AuthStateContext.Consumer>
);
}
}
Reference
Context Consumer
Related
I am a newbie in React and Next JS, I want to set initial auth user data on initial load from the __app.js. But using dispatch throwing error "Invalid hook call". I know according to docs calling hooks in render function is wrong. but I am looking for an alternate solution to this.
How I can set auth data one-time so that will be available for all the pages and components.
I am including my code below.
/contexts/app.js
import { useReducer, useContext, createContext } from 'react'
const AppStateContext = createContext()
const AppDispatchContext = createContext()
const reducer = (state, action) => {
switch (action.type) {
case 'SET_AUTH': {
return state = action.payload
}
default: {
throw new Error(`Unknown action: ${action.type}`)
}
}
}
export const AppProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, {})
return (
<AppDispatchContext.Provider value={dispatch}>
<AppStateContext.Provider value={state}>
{children}
</AppStateContext.Provider>
</AppDispatchContext.Provider>
)
}
export const useAuth = () => useContext(AppStateContext)
export const useDispatchAuth = () => useContext(AppDispatchContext)
/_app.js
import 'bootstrap/dist/css/bootstrap.min.css'
import '../styles/globals.css'
import App from 'next/app'
import Layout from '../components/Layout'
import { mutate } from 'swr'
import { getUser } from '../requests/userApi'
import { AppProvider, useDispatchAuth } from '../contexts/app'
class MyApp extends App {
render() {
const dispatchAuth = useDispatchAuth()
const { Component, pageProps, props } = this.props
// Set initial user data
const setInitialUserData = async () => {
if (props.isServer) {
const initialData = {
loading: false,
loggedIn: (props.user) ? true : false,
user: props.user
}
const auth = await mutate('api-user', initialData, false)
dispatchAuth({
type: 'SET_AUTH',
payload: auth
})
}
}
//----------------------
// Set initial user data
setInitialUserData()
//----------------------
return (
<AppProvider>
<Layout>
<Component {...pageProps} />
</Layout>
</AppProvider>
)
}
}
MyApp.getInitialProps = async (appContext) => {
let isServer = (appContext.ctx.req) ? true : false
let user = null
let userTypes = {}
// Get user server side
if (isServer) {
await getUser()
.then(response => {
let data = response.data
if (data.status == true) {
// Set user
user = data.data.user
userTypes = data.data.user_types
//---------
}
})
.catch(error => {
//
})
}
//---------------------
return {
props: {
user,
userTypes,
isServer
}
}
}
export default MyApp
I believe this is the intended use of the useEffect hook with an empty array as its second argument:
https://reactjs.org/docs/hooks-effect.html
import {useEffect} from 'react'
class MyApp extends App {
useEffect(()=> {
setInitialUserData()
},[])
render() {
...
}
}
I have a React app with a currency unit switch. I have a function to switch the unit and update redux so that every component that has called the unit will be re-rendered. The problem is the redux prop (storedCurrencyUnit) is UNDEFINED whenever I updated the value and call the update function to redux.
Switch component
import { compose } from 'recompose';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { updateCurrencyUnit } from '../../store/actions';
class FrontHeader extends Component {
handleCurrencyChange = (e) => {
const { updateCurrencyUnit, storedCurrencyUnit } = this.props;
updateCurrencyUnit(e.target.checked)
console.log("unit", storedCurrencyUnit) // this is UNDEFINED
this.setState({ aud: e.target.checked }, () => {
localStorage.setItem("currencyUnit", this.state.aud ? "AUD" : "USD")
})
}
render() {
return (
<Switch
checked={this.state.aud}
onChange={this.handleCurrencyChange}
color="secondary"
name="aud"
inputProps={{ 'aria-label': 'currencyUnit' }}
/>
)
}
}
const mapStateToProps = (state) => ({
storedCurrencyUnit: state.storedCurrencyUnit
})
const mapDispatchToProps = (dispatch) => {
return bindActionCreators({
updateCurrencyUnit: updateCurrencyUnit,
}, dispatch);
}
export default compose(connect(mapStateToProps, mapDispatchToProps))(FrontHeader);
currencyReducer.js
const storedCurrencyUnit = (state = null, action) => {
switch (action.type) {
case 'UPDATE_CURRENCYUNIT':
return action.payload;
default:
return state;
}
}
export default storedCurrencyUnit;
actions.js
export const updateCurrencyUnit = (updatedCurrencyUnit) => {
return {
type: 'UPDATE_CURRENCYUNIT',
payload: updatedCurrencyUnit,
}
}
How can I solve this?
You need to dispatch the action using dispatcher. only that will maintain the promise and let know the redux store.
this.props.dispatch(updateCurrencyUnit("some value"));
WebContext.js
import React, { createContext, Component } from 'react';
export const WebContext = createContext();
class WebContextProvider extends Component {
state = {
inputAmount: 1,
};
render() {
return <WebContext.Provider value={{ ...this.state }}>{this.props.children}</WebContext.Provider>;
}
}
export default WebContextProvider;
App.js
const App = () => {
return (
<WebContextProvider>
<UpdateBtn />
</WebContextProvider>
);
};
export default App;
UpdateBtn.js
const UpdateBtn = () => {
return (
<Div>
<Button onClick={} />
</Div>
);
};
export default UpdateBtn;
How do I update the inputAmount state present in WebContext.js on button click in UpdateBtn.js? App.js is the parent component for UpdateBtn.js Also, How can I convert the WebContext.js into a functional component?
You should pass the function in Provider which you can call to update the value:
WebContext.js
import React, { createContext, Component } from 'react';
export const WebContext = createContext();
class WebContextProvider extends Component {
state = {
inputAmount: 1,
};
render() {
return (
<WebContext.Provider
value={{
data: ...this.state, // all data now in context.data field
update: () => { // we added this callback
this.setState((state) => ({
inputAmount: state.inputAmount + 1,
}));
},
}}
>
{this.props.children}
</WebContext.Provider>
);
}
}
export default WebContextProvider;
App.js
const App = () => {
return (
<WebContextProvider>
<UpdateBtn />
</WebContextProvider>
);
};
export default App;
UpdateBtn.js
const UpdateBtn = () => {
const context = useContext(WebContext); // we use hook to get context value
return (
<Div>
<Button onClick={context.update} />
</Div>
);
};
export default UpdateBtn;
or
const UpdateBtn = () => {
// or we can use Consumer to get context value
return (
<Div>
<WebContext.Consumer>
{context => <Button onClick={context.update} />}
</WebContext.Consumer>
</Div>
);
};
export default UpdateBtn;
An alternative approach might be to use a reducer to update your state. For example:
export const initialState = {
inputValue: 1
}
export function reducer(state, action) {
const { type, payload } = action;
switch (type) {
case 'updateInputValue': {
return { ...state, inputValue: payload };
}
default: return state;
}
}
Import those into your provider file:
import { initialState, reducer } from './reducer';
and use useReducer to create a store:
export function WebContextProvider({ children }) {
const store = useReducer(reducer, initialState);
return (
<WebContext.Provider value={store}>
{children}
</WebContext.Provider>
);
}
You can then import the context into the component that needs it and use useContext to get at the state and dispatch method. On the click of the button you can dispatch a new value to the store to update inputValue.
export default function UpdateButton() {
const [ { inputValue }, dispatch ] = useContext(WebContext);
function handleClick(e) {
dispatch({
type: 'updateInputValue',
payload: inputValue + 1
});
}
return (
<div>
<div>{inputValue}</div>
<button onClick={handleClick}>Click</button>
</div>
);
};
I've created a full demo to show you how it works in harmony.
I have a route to a component HandlingIndex:
<Route strict path={handlingCasePath} component={HandlingIndex} />
HandlingIndex is wrapped with a trackRouteParam component. trackRouteParam component looks like this:
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withRouter } from 'react-router-dom';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { parseQueryString } from '../../utils/urlUtils';
const defaultConfig = {
paramName: '',
parse: a => a,
paramPropType: PropTypes.any,
storeParam: () => undefined,
getParamFromStore: () => undefined,
isQueryParam: false,
paramsAreEqual: (paramFromUrl, paramFromStore) => paramFromUrl === paramFromStore
};
/**
* trackRouteParam
*
* Higher order component that tracks a route parameter and stores in the application
* state whenever it changes.
* #param config
*/
const trackRouteParam = config => (WrappedComponent) => {
class RouteParamTrackerImpl extends Component {
constructor() {
super();
this.updateParam = this.updateParam.bind(this);
}
componentDidMount() {
this.updateParam();
}
componentDidUpdate(prevProps) {
this.updateParam(prevProps.paramFromUrl);
}
componentWillUnmount() {
const { storeParam } = this.props;
storeParam(undefined);
}
updateParam(prevParamFromUrl) {
const { paramFromUrl, storeParam, paramsAreEqual } = this.props;
if (!paramsAreEqual(paramFromUrl, prevParamFromUrl)) {
storeParam(paramFromUrl);
}
}
render() {
const {
paramFromUrl,
paramFromStore,
storeParam,
paramsAreEqual,
...otherProps
} = this.props;
return <WrappedComponent {...otherProps} />;
}
}
const trackingConfig = { ...defaultConfig, ...config };
RouteParamTrackerImpl.propTypes = {
paramFromUrl: trackingConfig.paramPropType,
paramFromStore: trackingConfig.paramPropType,
storeParam: PropTypes.func.isRequired,
paramsAreEqual: PropTypes.func.isRequired
};
RouteParamTrackerImpl.defaultProps = {
paramFromUrl: undefined,
paramFromStore: undefined
};
const mapStateToProps = state => ({ paramFromStore: trackingConfig.getParamFromStore(state) });
const mapDispatchToProps = dispatch => bindActionCreators({ storeParam: trackingConfig.storeParam }, dispatch);
const mapMatchToParam = (match, location) => {
const params = trackingConfig.isQueryParam ? parseQueryString(location.search) : match.params;
return trackingConfig.parse(params[trackingConfig.paramName]);
};
const mergeProps = (stateProps, dispatchProps, ownProps) => ({
...ownProps,
...stateProps,
...dispatchProps,
paramFromUrl: mapMatchToParam(ownProps.match, ownProps.location),
paramsAreEqual: trackingConfig.paramsAreEqual
});
const RouteParamTracker = withRouter(connect(mapStateToProps, mapDispatchToProps, mergeProps)(RouteParamTrackerImpl));
RouteParamTracker.WrappedComponent = WrappedComponent;
Object.keys(RouteParamTracker).forEach((ownPropKey) => {
RouteParamTracker[ownPropKey] = WrappedComponent[ownPropKey];
});
return RouteParamTracker;
};
export default trackRouteParam;
In the component HandlingIndex, I am trying to get a param caseNumber from the url. Just showing the relevant parts here from the component:
const mapStateToProps = state => ({
selectedCaseNumber: getSelectedCaseNumber(state)
});
export default trackRouteParam({
paramName: 'caseNumber',
parse: caseNumberFromUrl => Number.parseInt(caseNumberFromUrl , 10),
paramPropType: PropTypes.number,
storeParam: setSelectedCaseNumber,
getParamFromStore: getSelectedCaseNumber
})(connect(mapStateToProps)(requireProps(['selectedCaseNumber'])(HandlingIndex)));
Action creator for the setSelectedCaseNumber is:
export const setSelectedCaseNumber= caseNumber=> ({
type: SET_SELECTED_CASE_NUMBER,
data: caseNumber
});
So, when I am going to the route 'case/1234', where the parameter is caseNumber: 1234 where I am setting the selectedCaseNumber I see that the data field is NaN. On inspecting the console, I can see that I in the function:
const mapMatchToParam = (match, location) => {
const params = trackingConfig.isQueryParam ? parseQueryString(location.search) : match.params;
return trackingConfig.parse(params[trackingConfig.paramName]);
};
I can see that match.params is an empty object.
I am not sure why is that, why I am getting an empty object?
In trackRouteParam HOC,
At line:
const RouteParamTracker = withRouter(connect(mapStateToProps, mapDispatchToProps, mergeProps)(RouteParamTrackerImpl));
You try edit:
const RouteParamTracker = connect(mapStateToProps, mapDispatchToProps, mergeProps)(withRouter(RouteParamTrackerImpl));
Hope can help you!
Pretty new to Redux. I'm trying to pass a handleClick event as a prop from a container component to a presentational component, the handleClick event is supposed to call upon an action which has been received as a prop with mapDispatchToProps.
Could someone tell me how to do this correctly please?
I'm building a calculator, just started, this only has three actions so far, add, Record_input_1 and Record_Input_2.
containers/ButtonsContainer.js:
import React, { Component } from 'react';
import { Buttons } from '../components/Buttons'
import { Record_Input_1 } from '../actions/sum-action';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
class ButtonsContainer extends Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick(num) {
return this.props.onRecordInput1(num)
}
render() {
return(
<Buttons handleClick={this.handleClick} />
)
}
mapStateToProps = (state) => {
return {
inputValue1: state.inputValue1,
inputValue2: state.inputValue2,
answer: state.answer
}
}
mapDispatchToProps = (dispatch) => {
return bindActionCreators({
onRecordInput1: Record_Input_1,
onRecordInput2: Record_Input_2
}, dispatch);
}
}
export default connect(mapStateToProps, mapDispatchToProps)(ButtonsContainer);
components/Buttons.js
import React, { Component } from 'react';
class Buttons extends Component {
render() {
const buttonMaker = (buttons, row) => {
for (let value of buttons) {
row.push(<button onClick={() => this.props.handleClick(value)} key={value}>
{value}
</button> )
}
}
let row1 = [];
let buttons1 = [1,2,3]
buttonMaker(buttons1, row1)
let row2 = [];
let buttons2 = [4,5,6]
buttonMaker(buttons2, row2)
let row3 = [];
let buttons3 = [7,8,9]
buttonMaker(buttons3, row3)
return (
<div>
<div>{row1}</div>
<br />
<div>{row2}</div>
<br />
<div>{row3}</div>
</div>
)
}
}
export default Buttons;
actions/sum-actions/js:
export const ADD = 'ADD';
export const RECORD_INPUT_1 = 'RECORD_INPUT_1';
export const RECORD_INPUT_2 = 'RECORD_INPUT_2';
export const add = (newInput1, newInput2) => {
return {
type: ADD,
newAnswer: newInput1 + newInput2
}
}
export const Record_Input_1 = (newInput1) => {
return {
type: RECORD_INPUT_1,
newInput1
}
}
export const Record_Input_2 = (newInput2) => {
return {
type: RECORD_INPUT_2,
newInput2
}
}
reducders/sum-reducer.js:
import { ADD, RECORD_INPUT_1, RECORD_INPUT_2 } from '../actions/sum-action'
export const initialState = {
inputValue1: '',
inputValue2: '',
answer: 0
}
export const sumReducer = (state = initialState, action) => {
switch (action.type) {
case ADD:
return [
...state,
{
answer: action.newAnswer
}
]
case RECORD_INPUT_1:
return [
...state,
{
inputValue1: action.newInput1
}
]
case RECORD_INPUT_2:
return [
...state,
{
inputValue2: action.newInput2
}
]
default:
return state;
}
}
store.js:
import { combineReducers, createStore } from 'redux';
import { initialState, sumReducer } from './reducers/sum-reducer';
const rootReducers = combineReducers({
sumReducer
})
export default createStore(rootReducers, initialState, window.devToolsExtension && window.devToolsExtension());
The buttons display ok, when I click on one I get this error:
TypeError: _this2.props.handleClick is not a function
for:
8 | render() {
9 | const buttonMaker = (buttons, row) => {
10 | for (let value of buttons) {
> 11 | row.push(<button onClick={() => this.props.handleClick(value)} key={value}
12 | {value}
13 | </button> )
14 | }
You are declaring mapStateToProps and mapDispatchToProps within ButtonsContainer. You are then passing those two methods to react-redux's connect as if they were declared outside of ButtonsContainer, hence they are undefined. Try moving them out of ButtonsContainer as shown here. It should look something like this:
class ButtonsContainer extends Component {
...
}
const mapStateToProps = (state) => {
return {
inputValue1: state.inputValue1,
inputValue2: state.inputValue2,
answer: state.answer
}
}
const mapDispatchToProps = (dispatch) => {
return bindActionCreators({
onRecordInput1: Record_Input_1,
onRecordInput2: Record_Input_2
}, dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(ButtonsContainer);