index.jsx
import React from 'react'
import { render } from 'react-dom'
import Main from './Main'
window.DatoCmsPlugin.init((plugin) => {
plugin.startAutoResizer()
const container = document.createElement('div')
document.body.appendChild(container)
render(<Main plugin={plugin} />, container)
})
Main.jsx
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import connectToDatoCms from './connectToDatoCms';
import './style.sass';
#connectToDatoCms(plugin => ({
developmentMode: plugin.parameters.global.developmentMode,
fieldValue: plugin.getFieldValue(plugin.fieldPath),
}))
export default class Main extends Component {
static propTypes = {
fieldValue: PropTypes.bool.isRequired,
}
render() {
const { fieldValue } = this.props;
return (
<div className="container">
{JSON.stringify(fieldValue)}
</div>
);
}
}
connectToDatoCms.jsx
import React, { Component } from 'react'
export default mapPluginToProps => BaseComponent => (
class ConnectToDatoCms extends Component {
constructor (props) {
super(props)
this.state = mapPluginToProps(props.plugin)
}
componentDidMount () {
const { plugin } = this.props
this.unsubscribe = plugin.addFieldChangeListener(plugin.fieldPath, () => {
this.setState(mapPluginToProps(plugin))
})
}
componentWillUnmount () {
this.unsubscribe()
}
render () {
return <BaseComponent {...this.props} {...this.state} />
}
}
)
I used this command to generate a starter code for datoCMS plugin, npx -p yo -p generator-datocms-plugin -c 'yo datocms-plugin'.
What is # in #connectToDatoCms, Main.jsx.
#connectToDatoCms uses the decorator pattern.
Your webpack config in your code is setup to process decorators, probably using babel-plugin-transform-decorators
Decorators are similar to HOCs
Decorators are just a wrapper around a function. They are used to enhance the functionality of the function without modifying the underlying function.
With the current HOC syntax pattern the above could have been used as
import connectToDatoCms from './connectToDatoCms';
import './style.sass';
class Main extends Component {
static propTypes = {
fieldValue: PropTypes.bool.isRequired,
}
render() {
const { fieldValue } = this.props;
return (
<div className="container">
{JSON.stringify(fieldValue)}
</div>
);
}
}
const mapPluginToProps = plugin => ({
developmentMode: plugin.parameters.global.developmentMode,
fieldValue: plugin.getFieldValue(plugin.fieldPath),
})
export default connectToDatoCms(mapPluginToProps)(Main);
Related
Login.js in functional component
import React, { useState } from 'react';
import { connect } from 'react-redux';
import { login } from '../actions/auth';
const Login = ({ login, isAuthenticated }) => {
return (
<div>
// some code here
</div>
);
};
const mapStateToProps = state => ({
isAuthenticated: state.auth.isAuthenticated
});
export default connect(mapStateToProps, { login })(Login);
How can I use above mapStateToProps function in class component as I used above in functional component?
Login.js in class component
import React, {Component} from "react";
class Login extends Component{
constructor(props) {
super(props);
this.state = {
search:'',
}
}
render() {
return(
<div>
//some code here
</div>
)
}
}
export default Login;
In class components mapStateToProps works the same as functional components, I feel there are only differences in some syntax or calling approaches.
It does not matter if a mapStateToProps function is written using the function keyword (function mapState(state) { } ) or as an arrow function
(const mapState = (state) => { } ) - it will work the same either way.
Class component with mapStateToProps
import React, {Component} from "react";
import { connect } from 'react-redux';
import { login } from '../actions/auth';
class Login extends Component{
constructor(props) {
super(props);
this.state = {
search:'',
}
}
render() {
const { isAuthenticated } = this.props;
return(
<div>
//some code here
</div>
)
}
}
function mapStateToProps(state) {
const { isAuthenticated } = state.auth.isAuthenticated
return { isAuthenticated }
}
export default connect(mapStateToProps)(Login)
As you can see there is only 1 major difference is: In Class component we are reading the value isAuthenticated from this.props whereas in the Functional component we are getting the value as arguments.
For information read more about mapStateToProps. https://react-redux.js.org/using-react-redux/connect-mapstate
Testing with Jasmine and Enzyme and I've been trying to test a HOC which is connected to redux. Take for instance the following HOC:
import React, { Component } from 'react';
import { connect } from 'react-redux';
const WithAreas = (WrappedComponent) => {
class WithAreasComponent extends Component {
constructor(props) {
super(props);
}
shouldRenderWrappedComponent(userObj) {
return !!userObj.areas;
}
render() {
const { userObj } = this.props;
return shouldRenderWrappedComponent(userObj)
? <WrappedComponent {...this.props} />
: null;
}
}
function mapStateToProps(state) {
const { info } = state;
const { userObj } = info.userObj;
return { userObj };
}
return connect(mapStateToProps)(WithAreasComponent);
};
export default WithAreas;
Let's say I want to test this HOC in order to check if the wrapped component is being render according to the userObj. I thought about doing a mock component and pass it to the HOC, but this is not working.
Test.js File:
import React from 'react';
import { shallow } from 'enzyme';
import jasmineEnzyme from 'jasmine-enzyme';
import WithAreas from './';
class MockComponent extends React.Component {
render() {
return(
<div> MOCK </div>
);
}
}
function setup(extraProps) {
const props = {
info: {
userObj: {
id: 'example1'
}
},
};
Object.assign(props, extraProps);
const WithAreasInstance = WithAreas(MockComponent);
const wrapper = shallow(<WithAreasInstance {...props} />);
return {
props,
wrapper
};
}
fdescribe('<WithAreas />', () => {
beforeEach(() => {
jasmineEnzyme();
});
it('should render the Mock Component', () => {
const { wrapper } = setup();
expect(wrapper.find(MockComponent).exists()).toBe(true);
});
});
But it gives me this error:
TypeError: (0 , _.WithAreas) is not a function
at setup (webpack:///src/user/containers/WithAreas/test.js:20:47 <- src/test_index.js:9:18060087)
at UserContext.<anonymous> (webpack:///src/user/containers//WithAreas/test.js:34:24 <- src/test_index.js:9:18060822)
What am I doing wrong? Or what approach might you recommend?
Thanks for any given help.
You're not importing the HOC in test correctly. You've set it as the default export, but are using named export deconstruction.
Try this in your test file:
import WithAreas from './';
Also, don't forget to pass the store to your component in test, otherwise the connect HOC won't work as expected.
shallow(
<Provider store={store}>
<WithAreasInstance ...>
</Provider>)
I'm trying to lazy load routes in React by implementing the AsyncCompoment class as documented here Code Splitting in Create React App. Below is the es6 asyncComponent function from the tutorial:
import React, { Component } from "react";
export default function asyncComponent(importComponent) {
class AsyncComponent extends Component {
constructor(props) {
super(props);
this.state = {
component: null
};
}
async componentDidMount() {
const { default: component } = await importComponent();
this.setState({
component: component
});
}
render() {
const C = this.state.component;
return C ? <C {...this.props} /> : null;
}
}
return AsyncComponent;
}
I've written this function in typescript and can confirm that components are indeed being loaded lazily. The issue I face is that they are not being rendered. I was able to determine that the component object is always undefined in the componentDidMount hook:
//AsyncComponent.tsx
async componentDidMount() {
const { default: component } = await importComponent();
this.setState({
component: component
});
}
The object being returned from the importComponent function has the following properties:
{
MyComponent: class MyComponent: f,
__esModule: true
}
I modified the componentDidMount method to take the first property of this object, which is the MyComponent class. After this change my project is now lazy loading the components and rendering them properly.
async componentDidMount() {
const component = await importComponent();
this.setState({
component: component[Object.keys(component)[0]]
});
}
My best guess is that I have not written this line properly in typescript:
const { default: component } = await importComponent();
I'm calling the asyncComponent method like so:
const MyComponent = asyncComponent(()=>import(./components/MyComponent));
Anyone know how to implement the AsyncComponent in typescript? I'm not sure if simply getting the 0 index on the esModule object is the correct way to do it.
// AsyncComponent.tsx
import * as React from "react";
interface AsyncComponentState {
Component: null | JSX.Element;
};
interface IAsyncComponent {
(importComponent: () => Promise<{ default: React.ComponentType<any> }>): React.ComponentClass;
}
const asyncComponent: IAsyncComponent = (importComponent) => {
class AsyncFunc extends React.PureComponent<any, AsyncComponentState> {
mounted: boolean = false;
constructor(props: any) {
super(props);
this.state = {
Component: null
};
}
componentWillUnmount() {
this.mounted = false;
}
async componentDidMount() {
this.mounted = true;
const { default: Component } = await importComponent();
if (this.mounted) {
this.setState({
component: <Component {...this.props} />
});
}
}
render() {
const Component = this.state.Component;
return Component ? Component : <div>....Loading</div>
}
}
return AsyncFunc;
}
export default asyncComponent;
// Counter.tsx
import * as React from 'react';
import { RouteComponentProps } from 'react-router';
interface CounterState {
currentCount: number;
}
class Counter extends React.Component<RouteComponentProps<{}>, CounterState> {
constructor() {
super();
this.state = { currentCount: 0 };
}
public render() {
return <div>
<h1>Counter</h1>
<p>This is a simple example of a React component.</p>
<p>Current count: <strong>{this.state.currentCount}</strong></p>
<button onClick={() => { this.incrementCounter() }}>Increment</button>
</div>;
}
incrementCounter() {
this.setState({
currentCount: this.state.currentCount + 1
});
}
}
export default Counter;
//routes.tsx
import * as React from 'react';
import { Route } from 'react-router-dom';
import { Layout } from './components/Layout';
import { Home } from './components/Home';
import asyncComponent from './components/AsyncComponent';
const AsyncCounter = asyncComponent(() => import('./components/Counter'));
export const routes = <Layout>
<Route exact path='/' component={Home} />
<Route path='/counter' component={AsyncCounter} />
</Layout>;
I'm trying to fetch records from backend graphql service and render them with Array.map function. Unfortunately before they're loaded I get error because they are undefined. I tried to set default props on component but it didin't work. Do i have to check if everything is loaded or is there specific way to inject default values into those props. My code looks like that right now
import React from 'react';
import { graphql } from 'react-apollo';
import { fetchTasks } from '../../../graphql/tasks';
import { Dashboard } from '../components/Dashboard';
const propTypes = {
data: React.PropTypes.shape({
tasks: React.PropTypes.array
})
};
const defaultProps = {
data: {
tasks: []
}
};
class DashboardContainer extends React.Component {
render() {
const titles = this.props.data.tasks.map(task => task.title);
return(
<Dashboard
titles={titles}
/>
);
}
}
DashboardContainer.propTypes = propTypes;
DashboardContainer.defaultProps = defaultProps;
export default graphql(fetchTasks)(DashboardContainer);
Yes you have to check if the query has finished to load. You could go through this nice tutorial, where you build a pokemon app. The link points to the part where they show a basic query and how you check if it is loaded.
In your case it could look like this:
import React from 'react';
import { graphql } from 'react-apollo';
import { fetchTasks } from '../../../graphql/tasks';
import { Dashboard } from '../components/Dashboard';
const propTypes = {
data: React.PropTypes.shape({
tasks: React.PropTypes.array
})
};
const defaultProps = {
data: {
tasks: []
}
};
class DashboardContainer extends React.Component {
render() {
if (this.props.data.loading) {
return <div > Loading < /div>;
}
const titles = this.props.data.tasks.map(task => task.title);
return ( <
Dashboard titles = {
titles
}
/>
);
}
}
DashboardContainer.propTypes = propTypes;
DashboardContainer.defaultProps = defaultProps;
export default graphql(fetchTasks)(DashboardContainer);
I've got fairly simple react component (Link wrapper which adds 'active' class if route is active):
import React, { PropTypes } from 'react';
import { Link } from 'react-router';
const NavLink = (props, context) => {
const isActive = context.router.isActive(props.to, true);
const activeClass = isActive ? 'active' : '';
return (
<li className={activeClass}>
<Link {...props}>{props.children}</Link>
</li>
);
}
NavLink.contextTypes = {
router: PropTypes.object,
};
NavLink.propTypes = {
children: PropTypes.node,
to: PropTypes.string,
};
export default NavLink;
How am I supposed to test it? My only attempt was:
import NavLink from '../index';
import expect from 'expect';
import { mount } from 'enzyme';
import React from 'react';
describe('<NavLink />', () => {
it('should add active class', () => {
const renderedComponent = mount(<NavLink to="/home" />, { router: { pathname: '/home' } });
expect(renderedComponent.hasClass('active')).toEqual(true);
});
});
It doesn't work and returns TypeError: Cannot read property 'isActive' of undefined. It definitely needs some router mocking, but I have no idea how to write it.
Thanks #Elon Szopos for your answer but I manage to write something much more simple (following https://github.com/airbnb/enzyme/pull/62):
import NavLink from '../index';
import expect from 'expect';
import { shallow } from 'enzyme';
import React from 'react';
describe('<NavLink />', () => {
it('should add active class', () => {
const context = { router: { isActive: (a, b) => true } };
const renderedComponent = shallow(<NavLink to="/home" />, { context });
expect(renderedComponent.hasClass('active')).toEqual(true);
});
});
I have to change mount to shallow in order not to evaluate Link which gives me an error connected with the react-router TypeError: router.createHref is not a function.
I would rather have "real" react-router than just an object but I have no idea how to create it.
For react router v4 you can use a <MemoryRouter>. Example with AVA and Enzyme:
import React from 'react';
import PropTypes from 'prop-types';
import test from 'ava';
import { mount } from 'enzyme';
import sinon from 'sinon';
import { MemoryRouter as Router } from 'react-router-dom';
const mountWithRouter = node => mount(<Router>{node}</Router>);
test('submits form directly', t => {
const onSubmit = sinon.spy();
const wrapper = mountWithRouter(<LogInForm onSubmit={onSubmit} />);
const form = wrapper.find('form');
form.simulate('submit');
t.true(onSubmit.calledOnce);
});
Testing components which rely on the context can be a little tricky. What I did was to write a wrapper that I used in my tests.
You can find the wrapper below:
import React, { PropTypes } from 'react'
export default class WithContext extends React.Component {
static propTypes = {
children: PropTypes.any,
context: PropTypes.object
}
validateChildren () {
if (this.props.children === undefined) {
throw new Error('No child components were passed into WithContext')
}
if (this.props.children.length > 1) {
throw new Error('You can only pass one child component into WithContext')
}
}
render () {
class WithContext extends React.Component {
getChildContext () {
return this.props.context
}
render () {
return this.props.children
}
}
const context = this.props.context
WithContext.childContextTypes = {}
for (let propertyName in context) {
WithContext.childContextTypes[propertyName] = PropTypes.any
}
this.validateChildren()
return (
<WithContext context={this.props.context}>
{this.props.children}
</WithContext>
)
}
}
Here you can see a sample usage:
<WithContext context={{ location: {pathname: '/Michael/Jackson/lives' }}}>
<MoonwalkComponent />
</WithContext>
<WithContext context={{ router: { isActive: true }}}>
<YourTestComponent />
</WithContext>
And it should work as you would expect.
You can use https://github.com/pshrmn/react-router-test-context for that exact purpose
"Create a pseudo context object that duplicates React Router's context.router structure. This is useful for shallow unit testing with Enzyme."
After installing it, you will be able to do something like
describe('my test', () => {
it('renders', () => {
const context = createRouterContext()
const wrapper = shallow(<MyComponent />, { context })
})
})