React Mobx can't display observable contents, very simple app - javascript

Very simple app, I'm trying to display content from my API using Mobx and Axios, here's my Axios agent.ts:
import { ITutorialUnit } from './../model/unit';
import axios, { AxiosResponse } from "axios";
//set the base URL
axios.defaults.baseURL = "http://localhost:5000/api";
//store our request in a const
const responseBody = (response: AxiosResponse) => response.data;
const requests = {
get: (url: string) => axios.get(url).then(responseBody),
};
//create a const for our activty's feature,all our activities' request are go inside our Activities object
const TutorialUnits = {
list: ():Promise<ITutorialUnit[]> => requests.get("/tutorialunits"),
};
export default{
TutorialUnits
}
then I call this agent.s in a store:
import { ITutorialUnit } from "./../model/unit";
import { action, observable } from "mobx";
import { createContext } from "react";
import agent from "../api/agent";
class UnitStore {
#observable units: ITutorialUnit[] = [];
//observable for loading indicator
#observable loadingInitial = false;
#action loadUnits = async () => {
//start the loading indicator
this.loadingInitial = true;
try {
//we use await to block anything block anything below list() method
const units = await agent.TutorialUnits.list();
units.forEach((unit) => {
this.units.push(unit);
// console.log(units);
});
this.loadingInitial = false;
} catch (error) {
console.log(error);
this.loadingInitial = false;
}
};
}
export default createContext(new UnitStore());
then I call this in my App component:
import React, { Fragment, useContext, useEffect } from "react";
import { Container } from "semantic-ui-react";
import "semantic-ui-css/semantic.min.css";
import NavBar from "../../features/nav/NavBar";
import { ActivityDashboard } from "../../features/Units/dashboard/tutorialUnitDashboard";
import UnitStore from "../stores/unitStore";
import { observer } from "mobx-react-lite";
import { LoadingComponent } from "./LoadingComponent";
const App = () => {
const unitStore = useContext(UnitStore);
useEffect(() => {
unitStore.loadUnits();
//need to specify the dependencies in dependenciy array below
}, [unitStore]);
//we are also observing loading initial below
if (unitStore.loadingInitial) {
return <LoadingComponent content="Loading contents..." />;
}
return (
<Fragment>
<NavBar />
<Container style={{ marginTop: "7em" }}>
<ActivityDashboard />
</Container>
</Fragment>
);
};
export default observer(App);
Finally, I want to use this component to display my content:
import { observer } from "mobx-react-lite";
import React, { Fragment, useContext } from "react";
import { Button, Item, Label, Segment } from "semantic-ui-react";
import UnitStore from "../../../app/stores/unitStore";
const UnitList: React.FC = () => {
const unitStore = useContext(UnitStore);
const { units } = unitStore;
console.log(units)
return (
<Fragment>
{units.map((unit) => (
<h2>{unit.content}</h2>
))}
</Fragment>
);
};
export default observer(UnitList);
I can't see the units..
Where's the problem? My API is working, I tested with Postman.
Thanks!!

If you were using MobX 6 then you now need to use makeObservable method inside constructor to achieve same functionality with decorators as before:
class UnitStore {
#observable units: ITutorialUnit[] = [];
#observable loadingInitial = false;
constructor() {
// Just call it here
makeObservable(this);
}
// other code
}
Although there is new thing that will probably allow you to drop decorators altogether, makeAutoObservable:
class UnitStore {
// Don't need decorators now anywhere
units: ITutorialUnit[] = [];
loadingInitial = false;
constructor() {
// Just call it here
makeAutoObservable(this);
}
// other code
}
More info here: https://mobx.js.org/react-integration.html

the problem seems to be the version, I downgraded my Mobx to 5.10.1 and my mobx-react-lite to 1.4.1 then Boom everything's fine now.

Related

How do I make a client-side only component for GatsbyJS?

How do I create a component for Gatsby that will load on the client-side, not at build time?
I created this one and it renders with gatsby develop but not with the rendered server-side rendering
import React from 'react';
import axios from 'axios';
import adapter from 'axios-jsonp';
export default class Reputation extends React.Component<{}, { reputation?: number }> {
constructor(props) {
super(props);
this.state = {};
}
async componentDidMount() {
const response = await axios({
url: 'https://api.stackexchange.com/2.2/users/23528?&site=stackoverflow',
adapter
});
if (response.status === 200) {
const userDetails = response.data.items[0];
const reputation = userDetails.reputation;
this.setState({
reputation
});
}
}
render() {
return <span>{ this.state.reputation?.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") }</span>
}
}
If you don't want the component to be bundled in the main js file at build time, use loadable-components
Install loadable-components and use it as a wrapper for a component that wants to use a client-side only package. docs
import React, { Component } from "react";
import Loadable from "#loadable/component";
const LoadableReputation = Loadable(() =>
import("../components/Reputation")
);
const Parent = () => {
return (
<div>
<LoadableReputation />
</div>
);
};
export default Parent;
before render this component, make sure you have a window, to detect if there is a window object. I would write a hook for that:
function hasWindow() {
const [isWindow, setIsWindow] = React.useState(false);
React.useEffect(() => {
setIsWindow(true);
return ()=> setIsWindow(false);
}, []);
return isWindow;
}
In the parent component check if there is a window object:
function Parent(){
const isWindow = hasWindow();
if(isWindow){
return <Reputation />;
}
return null;
}

How do i access redux state from another react component?

I am developing a lottery statistics app that gets data from a csv loaded from an input then I was wanting to read this data to the redux store so I can use it across multiple components.
I have successfully saved the data to the redux store once I import the file and read it through Header.js and using an action, but I am not sure how to access this in other components like e.g. Main.js.
I feel like I am still confused on how react/redux all fits together. I'm sorry if this has been asked before but everything I looked up online I couldn't get to work.
// index.js
import React from "react";
import ReactDOM from "react-dom";
import { createStore, applyMiddleware } from "redux";
import { Provider } from "react-redux";
import thunk from "redux-thunk";
import reducers from "./reducers";
import App from "./components/App";
const store = createStore(reducers, applyMiddleware(thunk));
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.querySelector("#root")
);
// App.js
import React from "react";
import Header from "./Header";
import Main from "./Main";
const App = () => {
return (
<div>
<Header />
<Main />
<div className="numbers-for-draw"></div>
</div>
);
};
export default App;
// Header.js
import React from "react";
import { CSVReader } from "react-papaparse";
import { fetchData } from "../actions";
import { connect } from "react-redux";
class Header extends React.Component {
constructor(props) {
super(props);
this.fileInput = React.createRef();
}
handleReadCSV = data => {
this.props.fetchData(data);
console.log(this.props.data);
};
handleOnError = (err, file, inputElem, reason) => {
console.log(err);
};
handleImportOffer = () => {
this.fileInput.current.click();
console.log("Got to handleImportOffer");
};
render() {
return (
<header>
<CSVReader
onFileLoaded={this.handleReadCSV}
inputRef={this.fileInput}
style={{ display: "none" }}
onError={this.handleOnError}
/>
<button onClick={this.handleImportOffer}>Import</button>
</header>
);
}
}
//Map what is in the redux store (e.g. state) to props
const mapStateToProps = state => ({
data: state.data
});
export default connect(mapStateToProps, {
fetchData: fetchData
})(Header);
// Main.js
import React from "react";
import { fetchData } from "../actions";
import { connect } from "react-redux";
const Main = () => {
console.log("In main");
console.log(this.props.data); //Blows up here.
return <div>Main</div>;
};
//Map what is in the redux store (e.g. state) to props
const mapStateToProps = state => ({
data: state.data
});
export default connect(mapStateToProps, {
fetchData: fetchData
})(Main);
// actions/index.js
export const fetchData = data => dispatch => {
console.log("Action");
const lottoData = {
stringNumbers: [
"one",
"two",
"three",
...
],
allResults: [],
winningNumbers: [],
winningNumbersAsStrings: []
};
const localData = data.data;
localData.shift();
localData.forEach(line => {
const lineObject = {
draw: line[0],
drawDate: line[1],
ballOne: line[2],
ballTwo: line[3],
ballThree: line[4],
ballFour: line[5],
ballFive: line[6],
ballSix: line[7],
bonusBall: line[8],
bonusBall2: line[9],
powerBall: line[10]
};
lottoData.allResults.push(lineObject);
let nums = [];
nums.push(parseInt(line[2]));
nums.push(parseInt(line[3]));
nums.push(parseInt(line[4]));
nums.push(parseInt(line[5]));
nums.push(parseInt(line[6]));
nums.push(parseInt(line[7]));
nums.sort((a, b) => {
if (a < b) {
return -1;
} else if (a > b) {
return 1;
} else {
return 0;
}
});
lottoData.winningNumbers.push(nums);
lottoData.winningNumbersAsStrings.push(nums.toString());
});
dispatch({ type: "FETCH_DATA", payload: lottoData });
};
// lottoReducer.js
export default (state = {}, action) => {
switch (action.type) {
case "FETCH_DATA":
return action.payload;
default:
return state;
}
};
// reducers/index.js
import { combineReducers } from "redux";
import lottoReducer from "./lottoReducer";
export default combineReducers({
data: lottoReducer
});
I haven't tested your code, but it seems to me that the only problem is in your Main.js
While you use a function component and not a class, you shouldn't use this to access your props. The following should work as expected:
const Main = (props) => {
console.log("In main");
console.log(props.data);
return <div>Main</div>;
};
//Map what is in the redux store (e.g. state) to props
const mapStateToProps = state => ({
data: state.data
});
export default connect(mapStateToProps, {
fetchData: fetchData
})(Main);
In your main.js you used functional components so this.props doesn't work there. You must pass props to your component and console.log(props.data).

React: Context to pass state between two hierarchies of components

I am developing a website in which I want to be able to access the state information anywhere in the app. I have tried several ways of implementing state but I always get following error message:
Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.
Check the render method of SOS.
Here is my SOS->index.js file:
import React, { useContext } from 'react';
import axios from 'axios';
import CONST from '../utils/Constants';
import { Grid, Box, Container } from '#material-ui/core';
import { styled } from '#material-ui/styles';
import { Header } from '../Layout';
import ListItem from './ListItem';
import SOSButton from './SOSButton';
import FormPersonType from './FormPersonType';
import FormEmergencyType from './FormEmergencyType';
import StateContext from '../App';
import Context from '../Context';
export default function SOS() {
const { componentType, setComponentType } = useContext(Context);
const timerOn = false;
//'type_of_person',
const ambulance = false;
const fire_service = false;
const police = false;
const car_service = false;
//static contextType = StateContext;
const showSettings = event => {
event.preventDefault();
};
const handleComponentType = e => {
console.log(e);
//this.setState({ componentType: 'type_of_emergency' });
setComponentType('type_of_emergency');
};
const handleEmergencyType = new_emergency_state => {
console.log(new_emergency_state);
// this.setState(new_emergency_state);
};
const onSubmit = e => {
console.log('in OnSubmit');
axios
.post(CONST.URL + 'emergency/create', {
id: 1,
data: this.state //TODO
})
.then(res => {
console.log(res);
console.log(res.data);
})
.catch(err => {
console.log(err);
});
};
let component;
if (componentType == 'type_of_person') {
component = (
<FormPersonType handleComponentType={this.handleComponentType} />
);
} else if (componentType == 'type_of_emergency') {
component = (
<FormEmergencyType
handleComponentType={this.handleComponentType}
handleEmergencyType={this.handleEmergencyType}
emergencyTypes={this.state}
timerStart={this.timerStart}
onSubmit={this.onSubmit}
/>
);
}
return (
<React.Fragment>
<Header title="Send out SOS" />
<StateContext.Provider value="type_of_person" />
<Container component="main" maxWidth="sm">
{component}
</Container>
{/*component = (
<HorizontalNonLinearStepWithError
handleComponentType={this.handleComponentType}
/>*/}
</React.Fragment>
);
}
I would really appreciate your help!
Just for reference, the Context file is defined as follows:
import React, { useState } from 'react';
export const Context = React.createContext();
const ContextProvider = props => {
const [componentType, setComponentType] = useState('');
setComponentType = 'type_of_person';
//const [storedNumber, setStoredNumber] = useState('');
//const [functionType, setFunctionType] = useState('');
return (
<Context.Provider
value={{
componentType,
setComponentType
}}
>
{props.children}
</Context.Provider>
);
};
export default ContextProvider;
EDIT: I have changed my code according to your suggestions (updated above). But now I get following error:
TypeError: Cannot read property 'componentType' of undefined
Context is not the default export from your ../Context file so you have to import it as:
import { Context } from '../Context';
Otherwise, it's trying to import your Context.Provider component.
For your file structure/naming, the proper usage is:
// Main app file (for example)
// Wraps your application in the context provider so you can access it anywhere in MyApp
import ContextProvider from '../Context'
export default () => {
return (
<ContextProvider>
<MyApp />
</ContextProvider>
)
}
// File where you want to use the context
import React, { useContext } from 'react'
import { Context } from '../Context'
export default () => {
const myCtx = useContext(Context)
return (
<div>
Got this value - { myCtx.someValue } - from context
</div>
)
}
And for godsakes...rename your Context file, provider, and everything in there to something more explicit. I got confused even writing this.

Accessing consumed React.Context in Next.js getInitialProps using HOC

I am attempting to abstract my API calls by using a simple service that provides a very simple method, which is just an HTTP call. I store this implementation in a React Context, and use its provider inside my _app.js, so that the API is globally available, but I have a problem at actually consuming the context in my pages.
pages/_app.js
import React from 'react'
import App, { Container } from 'next/app'
import ApiProvider from '../Providers/ApiProvider';
import getConfig from 'next/config'
const { serverRuntimeConfig, publicRuntimeConfig } = getConfig()
export default class Webshop extends App
{
static async getInitialProps({ Component, router, ctx }) {
let pageProps = {}
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx)
}
return { pageProps }
}
render () {
const { Component, pageProps } = this.props
return (
<Container>
<ApiProvider endpoint={publicRuntimeConfig.api_endpoint}>
<Component {...pageProps} />
</ApiProvider>
</Container>
);
}
}
Services/Api.js
import fetch from 'unfetch'
function Api (config)
{
const apiUrl = config.endpoint;
async function request (url) {
return fetch(apiUrl + '/' + url);
};
this.decode = async function (code) {
const res = request('/decode?code=' + code);
const json = await res.json();
return json;
}
return this;
}
export default Api;
Providers/ApiProvider.js
import React, { Component } from 'react';
import Api from '../Services/Api';
const defaultStore = null;
class ApiProvider extends React.Component
{
state = {
api: null
};
constructor (props) {
super(props);
this.state.api = new Api({ endpoint: props.endpoint });
}
render () {
return (
<ApiContext.Provider value={this.state.api}>
{this.props.children}
</ApiContext.Provider>
);
}
}
export const ApiContext = React.createContext(defaultStore);
export default ApiProvider;
export const ApiConsumer = ApiContext.Consumer;
export function withApi(Component) {
return function withApiHoc(props) {
return (
<ApiConsumer>{ context => <Component {...props} api={context} /> }</ApiConsumer>
)
}
};
pages/code.js
import React, { Component } from 'react';
import Link from 'next/link';
import { withApi } from '../Providers/ApiProvider';
class Code extends React.Component
{
static async getInitialProps ({ query, ctx }) {
const decodedResponse = this.props.api.decode(query.code); // Cannot read property 'api' of undefined
return {
code: query.code,
decoded: decodedResponse
};
}
render () {
return (
<div>
[...]
</div>
);
}
}
let hocCode = withApi(Code);
hocCode.getInitialProps = Code.getInitialProps;
export default hocCode;
The problem is that I am unable to access the consumed context. I could just make a direct fetch call within my getInitialProps, however I wanted to abstract it by using a small function that also takes a configurable URL.
What am I doing wrong?
You can't access an instance of your provider in as static method getInitialProps, it was called way before the React tree is generated (when your provider is available).
I would suggest you to save an Singelton of your API in the API module, and consume it inside the getInitialProps method via regular import.
Or, you can inject it to your componentPage inside the _app getInitialProps, something like that:
// _app.jsx
import api from './path/to/your/api.js';
export default class Webshop extends App {
static async getInitialProps({ Component, router, ctx }) {
let pageProps = {}
ctx.api = api;
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx)
}
return { pageProps }
}
render () {
const { Component, pageProps } = this.props
return (
<Container>
<Component {...pageProps} />
</Container>
);
}
}
// PageComponent.jsx
import React, { Component } from 'react';
class Code extends React.Component
{
static async getInitialProps ({ query, ctx }) {
const decodedResponse = ctx.api.decode(query.code); // Cannot read property 'api' of undefined
return {
code: query.code,
decoded: decodedResponse
};
}
render () {
return (
<div>
[...]
</div>
);
}
}
export default Code;
Does it make sense to you?

Test React Component using Redux and react-testing-library

I am new to testing redux connected components in React and trying to figure out how to test them.
Currently I'm using react-testing-library and having trouble setting up the my renderWithRedux function to correctly setup redux.
Here is a sample component:
import React, { Component } from 'react'
import { connect } from 'react-redux'
class Sample extends Component {
constructor(props) {
super(props);
this.state = {
...
}
}
componentDidMount() {
//do stuff
console.log(this.props)
}
render() {
const { user } = this.props
return(
<div className="sample">
{user.name}
</div>
)
}
}
const mapStateToProps = state => ({
user: state.user
})
export default connect(mapStateToProps, {})(Sample);
Here is a sample test:
import React from 'react';
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import { render, cleanup } from 'react-testing-library';
import Sample from '../components/sample/'
const user = {
id: 1,
name: "John Smith"
}}
function reducer(state = user, action) {
//dont need any actions at the moment
switch (action.type) {
default:
return state
}
}
function renderWithRedux(
ui,
{ initialState, store = createStore(reducer, initialState) } = {}
) {
return {
...render(<Provider store={store}>{ui}</Provider>),
store,
}
}
afterEach(cleanup)
test('<Sample> example text', () => {
const { getByTestId, getByLabelText } = renderWithRedux(<Sample />)
expect(getByText(user.name))
})
The user prop value always results as undefined. I have re-written this a couple of ways but can't seem to get it to work. If I pass the user data directly as a prop to Sample component in the test, it still resolves to be undefined.
I am learning from the tutorials and examples via the offical docs, like this one: https://github.com/kentcdodds/react-testing-library/blob/master/examples/tests/react-redux.js
Any pointers, tips or solutions would be greatly appreciated!
You should wrap the component inside Provider, here is the simple example
import React from 'react';
import { render } from '#testing-library/react';
import '#testing-library/jest-dom';
import { Provider } from "react-redux";
import configureMockStore from "redux-mock-store";
import TestedComponent from '../index';
const mockStore = configureMockStore();
const store = mockStore({});
const renderTestedComponent = () => {
return render(
<Provider store={store}>
<TestedComponent />
</Provider>
);
};
describe('test TestedComponent components', () => {
it('should be render the component correctly', () => {
const { container } = renderTestedComponent();
expect(container).toBeInTheDocument();
});
});
**Unable to Fire event using #testing-library**
// demo.test.js
import React from 'react'
import { Provider } from "react-redux";
import '#testing-library/react/cleanup-after-each'
import '#testing-library/jest-dom/extend-expect'
import { render, fireEvent } from '#testing-library/react'
// this is used to fire the event
// import userEvent from "#testing-library/user-event";
//import 'jest-localstorage-mock';
import ChangePassword from './ChangePassword';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
test('test 1-> Update User password', () => {
// global store
const getState = {
authUser :{
user : {
email: "test#gmail.com",
id: 0,
imageURL: null,
name: "test Solutions",
roleId: 1,
roleName: "testRole",
userName: "testUserName"
},
loading: false,
showErrorMessage: false,
errorDescription: ""
}
}; // initial state of the store
// const action = { type: 'LOGIN_USER' };
// const expectedActions = [action];
// const store = mockStore(getState, expectedActions);
const onSaveChanges = jest.fn();
const changePassword = jest.fn();
const store = mockStore(getState);
const { queryByText, getByLabelText, getByText , getByTestId , getByPlaceholderText, } = render(
<Provider store={store}>
<ChangePassword
onSaveChanges={onSaveChanges}
changePassword={changePassword}
/>
</Provider>,
)
// test 1. check the title of component
expect(getByTestId('updateTitle')).toHaveTextContent('Update your password');
// test 2. chekck the inputfile
expect(getByPlaceholderText('Old Password')) //oldpassword
expect(getByPlaceholderText('New Password')) //newpassword
expect(getByPlaceholderText('Confirm New Password')) //confpassword
// change the input values
fireEvent.change(getByPlaceholderText("Old Password"), {
target: { value: "theOldPasword" }
});
fireEvent.change(getByPlaceholderText("New Password"), {
target: { value: "#Ab123456" }
});
fireEvent.change(getByPlaceholderText("Confirm New Password"), {
target: { value: "#Ab123456" }
});
// check the changed input values
expect(getByPlaceholderText('Old Password').value).toEqual("theOldPasword");
expect(getByPlaceholderText('New Password').value).toEqual("#Ab123456");
expect(getByPlaceholderText('Confirm New Password').value).toEqual("#Ab123456");
expect(getByText('Save Changes')); // check the save change button
// calling onSave function
//fireEvent.click(getByTestId('savechange'))
// userEvent.click(getByText('Save Changes'));
})

Categories

Resources