开玩笑 - 不变式失败:您不应该在 <Router> 之外使用 <Link>

Posted

技术标签:

【中文标题】开玩笑 - 不变式失败:您不应该在 <Router> 之外使用 <Link>【英文标题】:Jest - Invariant failed: You should not use <Link> outside a <Router> 【发布时间】:2021-01-16 17:04:03 【问题描述】:

我在运行测试时收到此错误,但是我已将我正在测试的组件包装在 &lt;BrowserRouter&gt; 组件中:

● Axios › 得到响应

Invariant failed: You should not use <Link> outside a <Router>

  91 | 
  92 |             if (RootComponent) 
> 93 |                 const component = mountWithCustomWrappers(<Wrapper store=store><RootComponent

...props />, rootWrappers);

nock.integration.test

import React from 'react';
import axios from 'axios';

const core = require('tidee-life-core');

import httpAdapter from 'axios/lib/adapters/http';
import doFetch from '../../../helpers/doFetch.js';
import ComponentBuilder from "../component-builder";
import LoginPage from "../../../scenes/login/login-page";

const host = 'http://example.com';
process.env.API_URL = host;
axios.defaults.host = host;
axios.defaults.adapter = httpAdapter;

const makeRequest = () => 
    return doFetch(
        url: core.urls.auth.login(),
        queryParams:  foo: 'bar' ,
    )
        .then(res => res.data)
        .catch(error => console.log(error));
;


describe('Axios', () => 
    let component;
    let componentBuilder;

    beforeEach(() => 
        componentBuilder = new ComponentBuilder();
    );

    test('gets a response', async () => 
        componentBuilder.includeInterceptor('login');
        component = await componentBuilder.build(
            RootComponent: LoginPage,
            selector: 'LoginForm',
        );

        return makeRequest()
            .then(response => 
                expect(typeof response).toEqual('object');
                expect(response.data.token).toEqual('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhNDY3MGE3YWI3ZWM0ZjQ2MzM4ODdkMzJkNzRkNTY5OSIsImlhdCI6MTU1MjA5MDI1NX0.vsKLXJEqSUZK-Y6IU9PumfZdW7t1SLM28jzJL89lcrA');
            );
    );
);

登录页面.jsx:

import React,  Component  from 'react';
import PropTypes from "prop-types";
import  withRouter  from 'react-router-dom';
import  injectIntl, intlShape  from 'react-intl';
import queryString from 'query-string';

import  Link  from 'tidee-life-ui';
import LoginForm from './login-form.jsx';
import  doLogin  from '../../api/auth/auth-api';
import Auth from '../../modules/Auth';
import messages from '../../messages';

const  urls  = require('tidee-life-core');


class LoginPage extends Component 

    constructor(props) 
        super(props);

        const  intl  = props;

        if (Auth.isUserAuthenticated()) 
            props.history.replace( pathname: urls.pages.pathBoxes() );
        

        this.messages = 
            'account.activation.error.expired': intl.formatMessage(messages['account.activation.error.expired']),
            'account.activation.required': intl.formatMessage(messages['account.activation.required']),
            'common.click': intl.formatMessage(messages['common.click']),
            'common.here': intl.formatMessage(messages['common.here']),
            'error.500.msg': intl.formatMessage(messages['error.500.msg']),
            'forgot.success': intl.formatMessage(messages['forgot.success']),
            'login.account.needs.activating.partial': intl.formatMessage(messages['login.account.needs.activating.partial']),
            'login.error.account.credentials': intl.formatMessage(messages['login.error.account.credentials']),
            'login.validation.email': intl.formatMessage(messages['login.validation.email']),
            'login.validation.password': intl.formatMessage(messages['login.validation.password']),
            'signup.account.created': intl.formatMessage(messages['signup.account.created'])
        ;

        let alertMessage;
        let alertMessageType;

        const query = queryString.parse(props.location.search);
        if ('signup-success' in query) 
            alertMessage = this.messages['signup.account.created'];
            alertMessageType = 'success';
         else if ('forgot-success' in query) 
            alertMessage = this.messages['forgot.success'];
            alertMessageType = 'success';
        

        this.state = 
            alert: 
                type: alertMessageType ? alertMessageType : '',
                msg: alertMessage ? alertMessage : '',
            ,
            user: 
                email: '',
                password: ''
            
        ;

        this.changeUser = this.changeUser.bind(this);
        this.clearAlert = this.clearAlert.bind(this);
        this.processForm = this.processForm.bind(this);
    

    clearAlert() 
        this.setState( alert: 
            type: '',
            msg: '',
        );
    

    processForm(e) 
        e.preventDefault();
        return doLogin(
            email: this.state.user.email,
            password: this.state.user.password,

        ).then((response) => 
            Auth.authenticateUser(response.data.token);
            this.props.history.replace( pathname: urls.pages.pathBoxes() );

        ).catch((error) => 
            const msg = error.message && this.messages[error.message] ? [this.messages[error.message]] : [this.messages['error.500.msg']];
            if (error.message === 'account.activation.error.expired' || error.message === 'account.activation.required') 
                const to = urls.pages.pathResendLink(error.data.confirmHash);
                msg.push(` $this.messages['common.click'] `);
                msg.push(<Link underline color="inherit" key="email" to=to>this.messages['common.here']</Link>);
                msg.push(` $this.messages['login.account.needs.activating.partial']`);
            

            this.setState(
                alert: 
                    type: 'error',
                    msg,
                
            );
        );
    

    changeUser(event) 
        const  name, value  = event.target;
        this.setState((currentState) => (
            user: 
                ...currentState.user,
                [name]: value,
            
        ));
    

    render() 
        return (
            <LoginForm
                data-test="login-form"
                alert=this.state.alert
                onSubmit=this.processForm
                onChange=this.changeUser
                user=this.state.user
            />
        );
    


LoginPage.propTypes = 
    history: PropTypes.object,
    intl: intlShape.isRequired,
    location: PropTypes.object.isRequired,
;

export default injectIntl(withRouter(LoginPage));

component-builder.js

import React from "react";
import nock from 'nock';
import cloneDeep from 'lodash.clonedeep';
import  mountWithCustomWrappers  from 'enzyme-custom-wrappers';

import  waitForStoreState  from './wait/wait-for-store-state';
import Wrapper from './wrapper.jsx';
import waitForComponentPredicate from './wait-for-component-predicate/wait-for-component-predicate';
import waitForComponentSelector from './wait-for-component-selector/wait-for-component-selector';
import  startAllNockServiceIntercepts  from './nock/nock-manager';
import nockServices from './nock/services';
import store from "../../store/store";
import wrappers from './wrappers';

const rootWrappers = component => wrappers(component);


class ComponentBuilder 

    constructor() 
        this.nockInterceptors = [];
        this.storePreparers = [];
    

    includeInterceptor( interceptorName, nockConfigOverride = null ) 
        // Maybe need to do a clone deep here if things start breaking!
        const clonedNockService = cloneDeep(nockServices[interceptorName]().nockConfig);
        const nockService = 
            [interceptorName]: 
                ...clonedNockService,
                ...(nockConfigOverride || ),
            
        ;

        this.nockInterceptors.push(nockService);
    

    prepareStore( storePreparer ) 
        this.storePreparers.push(storePreparer);
    

    async waitForStoreToUpdate() 
        const promises = this.storePreparers
            .map(service => waitForStoreState(service.redux.storeStateToWaitFor, store));

        await Promise.all(promises);
    

    async runStorePreparers() 
        nock.cleanAll();
        const interceptors = [];
        this.storePreparers.forEach((service) => 
            const interceptorName = service.http.interceptor;
            const clonedNockService = service.http.interceptor && cloneDeep(nockServices[interceptorName]().nockConfig);
            interceptors.push(
                [interceptorName]: 
                    ...clonedNockService,
                
            );
        );
        startAllNockServiceIntercepts(interceptors);

        this.storePreparers.forEach(service => service.redux.actionToDispatch && store.dispatch(service.redux.actionToDispatch()));

        return await this.waitForStoreToUpdate();
    

    /**
     * Build a component to be tested.
     * @param RootComponent
     * @param selector string - A selector to wait for. CSS selector or name of component.
     * @param props object
     * @param store object
     * @param predicate function - A function that returns true if a condition is met.
     * @param predicateMaxTime number
     * @param predicateInterval number
     * @returns Promise<*>
     */
    async build(
        RootComponent = null,
        selector = '',
        props = ,
        predicate = null,
        predicateMaxTime = 2000,
        predicateInterval = 10,
     = ) 
        try 
            await this.runStorePreparers();

            startAllNockServiceIntercepts(this.nockInterceptors);

            if (RootComponent) 
                const component = mountWithCustomWrappers(<Wrapper store=store><RootComponent ...props /></Wrapper>, rootWrappers);

                if (selector) 
                    await waitForComponentSelector( selector, rootComponent: component, store );
                

                if (predicate) 
                    await waitForComponentPredicate(
                        predicate,
                        rootComponent: component,
                        store,
                        maxTime: predicateMaxTime,
                        interval: predicateInterval,
                    );
                

                return component;
            
         catch(err) 
            throw err;
        
    


export default ComponentBuilder;

wrapper.jsx

import PropTypes from 'prop-types';
import React,  Component  from 'react';
import  BrowserRouter  from "react-router-dom";
import  Provider  from 'react-redux';

import ThemeProvider from "../../theme/Theme.jsx";
import LocaleProviderWrapper from "./locale-provider-wrapper.jsx";


const propTypes = 
    children: PropTypes.element.isRequired,
    store: PropTypes.object.isRequired,
;

class Wrapper extends Component 
    getStore() 
        return this.props.store;
    

    render() 
        return (
            <Provider store=this.props.store>
                <LocaleProviderWrapper>
                    <ThemeProvider>
                        <BrowserRouter>this.props.children</BrowserRouter>
                    </ThemeProvider>
                </LocaleProviderWrapper>
            </Provider>
        );
    


Wrapper.propTypes = propTypes;

export default Wrapper;

【问题讨论】:

【参考方案1】:

正如消息所说,您不能使用没有Router 类型的任何父级的Link。在 processForm 函数中,您正在使用已磨损的 Link 组件构建消息。

if (error.message === 'account.activation.error.expired' || error.message === 'account.activation.required') 
  const to = urls.pages.pathResendLink(error.data.confirmHash);
  msg.push(` $this.messages['common.click'] `);
  msg.push(<Link underline color="inherit" key="email" to=to>this.messages['common.here']</Link>);
  msg.push(` $this.messages['login.account.needs.activating.partial']`);

你应该使用a标签来建立动态链接。可能是这样的:

msg.push(`<a href="mailto:$this.to">$this.messages['common.here']</a>`);

【讨论】:

即使包含 组件的这段代码没有运行,是否会触发错误? 我相信是的,你可以把链接去掉,看看是否还有错误。 代码仍然需要从 JSX 编译成 javascript @JoeTidee 这是否解决了您的问题?

以上是关于开玩笑 - 不变式失败:您不应该在 <Router> 之外使用 <Link>的主要内容,如果未能解决你的问题,请参考以下文章

错误:不变量失败:您不应该在 <Router> 之外使用 <Link> 进行反应

React 测试库不变量失败:您不应该在 <Router> 之外使用 <Route>

不变违规:您不应该在 <Router> 之外使用 <Switch>

不变违规:您不应该在 <Router> 之外使用 withRouter() (使用最少的工作示例)

使用 Enzyme 测试 React 组件时出错:'不变违规:您不应在 <Router> 外使用 <Link>'

从共享组件库导出`react-router`重定向