I am really new in Hooks and during learning faces many difficulties to switch from the old style.
My old code looks like:
context.js
import React from "react";
const SomeContext = React.createContext(null);
export const withContext = (Component) => (props) => (
<SomeContext.Consumer>
{(server) => <Component {...props} server={server} />}
</SomeContext.Consumer>
);
export default SomeContext;
main index.js
<SomeContext.Provider value={new SomeClass()}>
<App />
</SomeContext.Provider>
but when I want to access it through with export default withContext(SomeComponent) by this.props.server.someFunc() it showed props is undefined in the classless hook function.
how can I achieve this.props in react hook
Edit:
SomeClass is not React inheriting class and its look like it.
class SomeClass {
someFunc = (id) => axios('api endpoints')
}
SomeComponent
const SomeComponent = () => {
...
useEffect(() => {
this.props.server.someFunc(id).then(...).catch(...)
}, ...)
...
}
In the regular case, you need to export the Context, then import it and use it within useContext:
export const SomeContext = React.createContext(null);
// Usage
import { SomeContext } from '..';
const SomeComponent = () => {
const server = useContext(SomeContext);
useEffect(() => {
server.someFunc(id);
});
};
But in your case, since you using HOC, you have the server within the prop it self:
const SomeComponent = (props) => {
useEffect(() => {
props.server.someFunc(id);
});
};
Related
Considering the following project setup on a react-redux application that uses context API to avoid prop drilling. The example given is simplified.
Project Setup
React project uses React Redux
Uses context API to avoid prop drilling in certain cases.
Redux store has a prop posts which contains list of posts
An action creator deletePost(), which deletes a certain post by post id.
To avoid prop drilling, both posts and deletePosts() is added to a context AppContext and returned by a hook funciton useApp().
posts array is passed via contexts so it is not used by connect() function. Important
Problem:
When action is dispatched store is updated however Component is not re-rendered (because the prop is not connected?). Of course, if I pass the prop with connect function and drill it down to child rendering works fine.
What is the solution?
Example Project
The example project can be found in codesandbox. Open up the console and try to click the delete button. You will see no change in the UI while you can see the state is updated in the console.
Codes
App.js
import Home from "./routes/Home";
import "./styles.css";
import { AppProvider } from "./context";
export default function App() {
return (
<AppProvider>
<div className="App">
<Home />
</div>
</AppProvider>
);
}
context.js
import { useDispatch, useStore } from "react-redux";
import { useContext, createContext } from "react";
import { deletePost } from "./redux/actions/posts";
export const AppContext = createContext();
export const useApp = () => {
return useContext(AppContext);
};
export const AppProvider = ({ children }) => {
const dispatch = useDispatch();
const {
posts: { items: posts }
} = useStore().getState();
const value = {
// props
posts,
// actions
deletePost,
dispatch
};
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
};
Home.js
import { connect } from "react-redux";
import Post from "../components/Post";
import { useApp } from "../context";
const Home = () => {
const { posts } = useApp();
return (
<section>
{posts.map((p) => (
<Post key={p.id} {...p} />
))}
</section>
);
};
/*
const mapProps = ({ posts: { items: posts } }) => {
return {
posts
};
};
*/
export default connect()(Home);
Post.js
import { useApp } from "../context";
const Post = ({ title, content, id }) => {
const { deletePost, dispatch } = useApp();
const onDeleteClick = () => {
console.log("delete it", id);
dispatch(deletePost(id));
};
return (
<article>
<h1>{title}</h1>
<p>{content}</p>
<div className="toolbar">
<button onClick={onDeleteClick}>Delete</button>
</div>
</article>
);
};
export default Post;
You're not using the connect higher order component method properly . Try using it like this so your component will get the states and the function of your redux store :
import React from 'react';
import { connect } from 'react-redux';
import { callAction } from '../redux/actions.js';
const Home = (props) => {
return (
<div> {JSON.stringify(props)} </div>
)
}
const mapState = (state) => {
name : state.name // name is in intialState
}
const mapDispatch = (dispatch) => {
callAction : () => dispatch(callAction()) // callAction is a redux action
//and should be imported in the component also
}
export default connect(mapState , mapDispatch)(Home);
You can access the states and the actions from your redux store via component props.
Use useSelector() instead of useState(). Example codepen is fixed.
Change from:
const { posts: { items: posts } } = useStore().getState();
Change to:
const posts = useSelector(state => state.posts.items);
useStore() value is only received when component is first mounted. While useSlector() will get value when value is changed.
I just started playing with context today and this is my usercontext
import { createContext, useEffect, useState } from "react";
import axios from "axios";
export const userContext = createContext({});
const UserContext = ({ children }) => {
const [user, setUser] = useState({});
useEffect(() => {
axios.get("/api/auth/user", { withCredentials: true }).then((res) => {
console.log(res);
setUser(res.data.user);
});
}, []);
return <userContext.Provider value={user}>{children}</userContext.Provider>;
};
export default UserContext;
this is how im using it in any component that needs the currently logged in user
const user = useContext(userContext)
my question is whenever the user logs in or logs out I have to refresh the page in order to see the change in the browser. is there any way that I can do this where there does not need to be a reload. also any general tips on react context are appreciated
(EDIT)
this is how Im using the UserContext if it helps at all
const App = () => {
return (
<BrowserRouter>
<UserContext>
<Switch>
{routes.map((route) => (
<Route
key={route.path}
path={route.path}
component={route.component}
/>
))}
</Switch>
</UserContext>
</BrowserRouter>
);
};
Where is your context consumer?
The way it is set up, any userContext.Consumer which has a UserContext as its ancestor will re render when the associated user is loaded, without the page needing to be reloaded.
To make it clearer you should rename your UserContext component to UserProvider and create a corresponding UserConsumer component:
import { createContext, useEffect, useState } from "react";
import axios from "axios";
export const userContext = createContext({});
const UserProvider = ({ children }) => {
const [user, setUser] = useState({});
useEffect(() => {
axios.get("/api/auth/user", { withCredentials: true }).then((res) => {
console.log(res);
// setting the state here will trigger a re render of this component
setUser(res.data.user);
});
}, []);
return <userContext.Provider value={user}>{children}</userContext.Provider>;
};
const UserConsumer = ({ children }) => {
return (
<userContext.Consumer>
{context => {
if (context === undefined) {
throw new Error('UserConsumer must be used within a UserProvider ')
}
// children is assumed to be a function, it must be used
// this way: context => render something with context (user)
return children(context)
}}
</userContext.Consumer>
);
};
export { UserProvider, UserConsumer };
Usage example:
import { UserConsumer } from 'the-file-containing-the-code-above';
export const SomeUiNeedingUserInfo = props => (
<UserConsumer>
{user => (
<ul>
<li>{user.firstName}</>
<li>{user.lastName}</>
</ul>
)}
</UserConsumer>
)
To be fair, you could also register to the context yourself, this way for a functional component:
const AnotherConsumer = props => {
const user = useContext(userContext);
return (....);
}
And this way for a class component:
class AnotherConsumer extends React.Component {
static contextType = userContext;
render() {
const user = this.context;
return (.....);
}
}
The benefit of the UserConsumer is reuasability without having to worry if you're in a functional or class component: it will used the same way.
Either way you have to "tell" react which component registers (should listen to) the userContext to have it refreshed on context change.
That's the whole point of context: allow for a small portion of the render tree to be affected and avoid prop drilling.
I want to test my pure component so I'm doing this:
MyComp.js
export MyComp = props => {
return (
<Wrapper>Content</Wrapper>
)
}
const MyCompConn = connect()(MyComp);
export default MyCompConn;
Where <Wrapper>:
export Wrapper = ({children}) => {
return (
<div>{children}</div>
)
}
const WrapperConn = connect()(Wrapper);
export default WrapperConn;
MyComp.test.js
import React from 'react';
import renderer from 'react-test-renderer';
import { MyComp } from '../../MyComp';
describe('With Snapshot Testing', () => {
it('renders!"', () => {
const component = renderer.create(<Login />);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
});
Now, when I run yarn test I get this error:
Invariant Violation: Could not find "store" in either the context or props of "Connect(AppWrapper)". Either wrap the root component in a <Provider> or explicitly pass "store" as a prop to "Connect(AppWrapper)"
And this is happening because <Wrapper> is connected in my <MyComp> component, but I'm not sure how to test just the latter even if it's wrapped on a connected component.
To test our component without using mocked store, we can mock the connect of react-redux itself using Jest. PFB the example:
import React from 'react';
import renderer from 'react-test-renderer';
import { MyComp } from '../../MyComp';
jest.mock('react-redux', () => ({
connect: () => {
return (component) => {
return component
};
}
}));
describe('With Snapshot Testing', () => {
it('renders!"', () => {
const component = renderer.create(<Login />);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
});
Now, this will render the Wrapper component directly, and not the connected Wrapper component.
Try this:
import configureMockStore from 'redux-mock-store';
const store = mockStore({});
const component = renderer.create(<Provider store={store}><Login /></Provider>);
This question already has answers here:
Access React Context outside of render function
(5 answers)
Closed 4 years ago.
So I need to access to the value of context without calling it as a render function.
NOT Like this:
const Child = () => {
return (
<ThemeConsumer>
{context => {return <Text>{context}</Text>}}
</ThemeConsumer>
);
};
I have this so far:
import React from 'react';
export const ThemeContext = React.createContext(0);
export const ThemeProvider = ThemeContext.Provider;
export const ThemeConsumer = ThemeContext.Consumer;
And I am using the provider like this:
render() {
const { index } = this.state;
return (
<ThemeProvider value={index}>
<TabView
key={Math.random()}
navigationState={this.state}
renderScene={this.renderScene}
renderTabBar={this.renderTabBar}
onIndexChange={this.handleIndexChange}
/>
</ThemeProvider>
);
}
Is that ok?
The main issue I have so far is that where I need that value, is not a class nor a functional component.
I need to do something like this:
import { ThemeConsumer } from '../../context/BottomNavigationTheme';
import HomeScreen from '../screens/HomeScreen';
import HomeScreen2 from '../screens/HomeScreen2';
functionToGetContextValue = () => valueFromContext;
const HomeStack = createStackNavigator({
Home: functionToGetContextValue ? HomeScreen : HomeScreen2, // HERE I NEED IT
});
HomeStack.navigationOptions = {
tabBarLabel: 'All Passengers',
tabBarIcon: ({ focused }) => <AllPassengersIcon focused={focused} />,
};
export default createBottomTabNavigator({
HomeStack,
LinksStack,
SettingsStack,
});
What should I do to access to that value?
You can with HOC. Create a function withTheme
export const withTheme = Component => props => (
<ThemeConsumer>
{context => <Component {...props} them={context} />
</ThemeConsumer>
)
Then use in any child component you want.
class TabView extends React.PureComponent {
componentDidMount() {
console.log(this.props.theme)
}
...
}
export default withTheme(TabView)
A working Context with HOC is here https://codesandbox.io/s/o91vrxlywy
If you use React hooks (ver 16.8) you can use useContext API https://reactjs.org/docs/hooks-reference.html#usecontext
I am trying to test my component which is consuming data from context via HOC.
Here is setup:
Mocked context module /context/__mocks__
const context = { navOpen: false, toggleNav: jest.fn() }
export const AppContext = ({
Consumer(props) {
return props.children(context)
}
})
Higher OrderComponent /context/withAppContext
import React from 'react'
import { AppContext } from './AppContext.js'
/**
* HOC with Context Consumer
* #param {Component} Component
*/
const withAppContext = (Component) => (props) => (
<AppContext.Consumer>
{state => <Component {...props} {...state}/>}
</AppContext.Consumer>
)
export default withAppContext
Component NavToggle
import React from 'react'
import withAppContext from '../../../context/withAppContext'
import css from './navToggle/navToggle.scss'
const NavToggle = ({ toggleNav, navOpen }) => (
<div className={[css.navBtn, navOpen ? css.active : null].join(' ')} onClick={toggleNav}>
<span />
<span />
<span />
</div>
)
export default withAppContext(NavToggle)
And finally Test suite /navToggle/navToggle.test
import React from 'react'
import { mount } from 'enzyme'
beforeEach(() => {
jest.resetModules()
})
jest.mock('../../../../context/AppContext')
describe('<NavToggle/>', () => {
it('Matches snapshot with default context', () => {
const NavToggle = require('../NavToggle')
const component = mount( <NavToggle/> )
expect(component).toMatchSnapshot()
})
})
Test is just to get going, but I am facing this error:
Warning: Failed prop type: Component must be a valid element type!
in WrapperComponent
Which I believe is problem with HOC, should I mock that somehow instead of the AppContext, because technically AppContext is not called directly by NavToggle component but is called in wrapping component.
Thanks in advance for any input.
So I solved it.
There were few issues with my attempt above.
require does not understand default export unless you specify it
mounting blank component returned error
mocking AppContext with __mock__ file caused problem when I wanted to modify context for test
I have solved it following way.
I created helper function mocking AppContext with custom context as parameter
export const defaultContext = { navOpen: false, toggleNav: jest.fn(), closeNav: jest.fn(), path: '/' }
const setMockAppContext = (context = defaultContext) => {
return jest.doMock('../context/AppContext', () => ({
AppContext: {
Consumer: (props) => props.children(context)
}
}))
}
export default setMockAppContext
And then test file ended looking like this
import React from 'react'
import { shallow } from 'enzyme'
import NavToggle from '../NavToggle'
import setMockAppContext, { defaultContext } from '../../../../testUtils/setMockAppContext'
beforeEach(() => {
jest.resetModules()
})
describe('<NavToggle/>', () => {
//...
it('Should have active class if context.navOpen is true', () => {
setMockAppContext({...defaultContext, navOpen: true})
const NavToggle = require('../NavToggle').default //here needed to specify default export
const component = shallow(<NavToggle/>)
expect(component.dive().dive().hasClass('active')).toBe(true) //while shallow, I needed to dive deeper in component because of wrapping HOC
})
//...
})
Another approach would be to export the component twice, once as decorated with HOC and once as clean component and create test on it, just testing behavior with different props. And then test just HOC as unit that it actually passes correct props to any wrapped component.
I wanted to avoid this solution because I didn't want to modify project file(even if it's just one word) just to accommodate the tests.