I'm pretty new to React and am trying to wrap my mind around Reflux.
Here's my scenario:
I currently have a very simple app that is calling some song data from a rest api, once retrieved, it is being stored in state.songs (this is happening within an Init function in the song store):
var SongStore = Reflux.createStore({
listenables: [SongActions],
getInitialState: function() {
return []
},
init: function() {
request('http://127.0.0.1:1337/api/songs', function(error, response, body) {
if (!error && response.statusCode == 200) {
var content = JSON.parse(body);
this.trigger(content);
}
}.bind(this));
},
onHandleClick: function(album_link) {
// this.state.songs is undefined here??
}
});
Here's the component that's rendering the song data:
var Home = React.createClass({
mixins: [Reflux.connect(SongStore, 'songs')],
render: function() {
return (
<div className={'react-app-home'}>
<div className="float-left">
<div className="song-data">
{this.state.songs.map(function(song) {
return <div><p>{song.lyrics}</p></div>
})}
</div>
</div>
<div className="float-right">
<AlbumList />
</div>
</div>
);
}
});
This is all working as intended.
I also have a list of albums, which is rendered in it's own component (and has it's own store), and i'm trying to hook up a click function on an album item, so once an album title is clicked, this.state.songs is filtered and the songs component is re-rendered.
The issue i'm having is when I try to access this.state.songs from the song store, it's undefined (see onHandleClick func in song store above).
Album list component:
var AlbumList = React.createClass({
mixins: [Reflux.connect(AlbumStore, 'albums'),
Reflux.connect(SongStore, 'songs')],
render: function() {
return (
<div className="album-list">
{this.state.albums.map(function(album) {
return <p onClick={SongStore.onHandleClick.bind(null, album.album_link)}>{album.album_name}</p>
})}
</div>
);
}
});
Album list store:
var AlbumStore = Reflux.createStore({
listenables: [AlbumActions],
getInitialState: function() {
return [];
},
onReload: function() {
request('http://0.0.0.0:1337/api/albums', function(error, response, body) {
if (!error && response.statusCode == 200) {
var content = JSON.parse(body);
this.trigger(content);
}
}.bind(this));
},
init: function() {
this.onReload();
}
});
Can anyone help me out? An answer with some explaining would be awesome so I can understand what the problem is.
Thanks!
You should be using actions for handleClick or any other call from the component to the store. I would also suggest separating your api calls from the store. Here is an example from https://github.com/calitek/ReactPatterns React.14/ReFluxSuperAgent.
app.js
'use strict';
import React from 'react';
import ReactDom from 'react-dom';
import AppCtrl from './components/app.ctrl.js';
import Actions from './actions/api.Actions';
import ApiStore from './stores/Api.Store';
window.ReactDom = ReactDom;
Actions.apiInit();
ReactDom.render( <AppCtrl />, document.getElementById('react') );
Note the action and api.store import.
Here is api.store.
import Reflux from 'reflux';
import Actions from '../actions/api.Actions';
import ApiFct from '../utils/sa.api';
let ApiStoreObject = {
newData: {
"React version": "0.14",
"Project": "ReFluxSuperAgent",
"currentDateTime": new Date().toLocaleString()
},
listenables: Actions,
apiInit() { ApiFct.setData(this.newData); },
apiInitDone() { ApiFct.getData(); },
apiSetData(data) { ApiFct.setData(data); }
}
const ApiStore = Reflux.createStore(ApiStoreObject);
export default ApiStore;
apiInit is doing a setData here but it could be the initial get calls.
Here it the api.
import request from 'superagent';
import apiActions from '../actions/api.Actions';
import saActions from '../actions/sa.Actions';
module.exports = {
getData() { request.get('/routes/getData').end((err, res) => { this.gotData(res.body); }); },
gotData(data) { saActions.gotData1(data); saActions.gotData2(data); saActions.gotData3(data); },
setData(data) { request.post('/routes/setData').send(data).end((err, res) => { apiActions.apiInitDone(); }) },
};
Here we use actions to pass the data to the store.
Here is the store.
import Reflux from 'reflux';
import Actions from '../actions/sa.Actions';
import AddonStore from './Addon.Store';
import MixinStoreObject from './Mixin.Store';
function _GotData(data) { this.data1 = data; BasicStore.trigger('data1'); }
let BasicStoreObject = {
init() { this.listenTo(AddonStore, this.onAddonTrigger); },
data1: {},
listenables: Actions,
mixins: [MixinStoreObject],
onGotData1: _GotData,
onAddonTrigger() { BasicStore.trigger('data2'); },
getData1() { return this.data1; },
getData2() { return AddonStore.data2; },
getData3() { return this.data3; }
}
const BasicStore = Reflux.createStore(BasicStoreObject);
export default BasicStore;
Note that reflux allows you to pass parameters in trigger.
Here is the component.
import React from 'react';
import BasicStore from '../stores/Basic.Store';
let AppCtrlSty = {
height: '100%',
padding: '0 10px 0 0'
}
const getState = () => {
return {
Data1: BasicStore.getData1(),
Data2: BasicStore.getData2(),
Data3: BasicStore.getData3()
};
};
class AppCtrlRender extends React.Component {
render() {
let data1 = JSON.stringify(this.state.Data1, null, 2);
let data2 = JSON.stringify(this.state.Data2, null, 2);
let data3 = JSON.stringify(this.state.Data3, null, 2);
return (
<div id='AppCtrlSty' style={AppCtrlSty}>
React 0.14 ReFlux with SuperAgent<br/><br/>
Data1: {data1}<br/><br/>
Data2: {data2}<br/><br/>
Data3: {data3}<br/><br/>
</div>
);
}
}
export default class AppCtrl extends AppCtrlRender {
constructor() {
super();
this.state = getState();
}
componentDidMount = () => { this.unsubscribe = BasicStore.listen(this.storeDidChange); }
componentWillUnmount = () => { this.unsubscribe(); }
storeDidChange = (id) => {
switch (id) {
case 'data1': this.setState({Data1: BasicStore.getData1()}); break;
case 'data2': this.setState({Data2: BasicStore.getData2()}); break;
case 'data3': this.setState({Data3: BasicStore.getData3()}); break;
default: this.setState(getState());
}
}
}
We are not using mixins. React classes are deprecating mixins so now is a good time to do without. It may seen like overkill to use an extra store and api util but good practices are always good practice.
Map allows passing this with an optional parameter map(function(item){}, this);
Related
React Testing Using Mock service worker is returning undefined and then taking values from the actual API. As you can see from the image down below the test is getting passed by getting values from the actual API and the name ARLO WANG is getting pulled out. Where as the name I have kept in the mockResponse is "first last" in the handler.js file.
FollowersList.js
import React, { useEffect, useState } from 'react'
import "./FollowersList.css"
import axios from "axios"
import { Link } from 'react-router-dom';
import { v4 } from 'uuid';
export default function FollowersList() {
const [followers, setFollowers] = useState([]);
useEffect(() => {
fetchFollowers()
}, []);
const fetchFollowers = async () => {
const {data} = await axios.get("https://randomuser.me/api/?results=5")
setFollowers(data.results)
}
// console.log(followers)
return (
<div className="followerslist-container">
<div>
{followers.map((follower, index) => (
<div className="follower-item" key={v4()} data-testid={`follower-item-${index}`}>
<div className="followers-details">
<div className="follower-item-name">
<h4>{follower.name.first}</h4> <h4>{follower.name.last}</h4>
</div>
<p>{follower.login.username}</p>
</div>
</div>
))}
</div>
<div className="todo-footer">
<Link to="/">Go Back</Link>
</div>
</div>
)
}
FollowersList.test.js
import { render, screen } from "#testing-library/react";
import { BrowserRouter } from "react-router-dom";
import FollowersList from "../FollowersList";
const MockFollowersList = () => {
return (
<BrowserRouter>
<FollowersList />
</BrowserRouter>
);
};
describe("FollowersList Component", () => {
test("renders first follower", async () => {
render(<MockFollowersList />);
screen.debug()
expect(await screen.findByTestId("follower-item-0")).toBeInTheDocument();
});
});
src/mock/handler.js
import { rest } from 'msw';
const mockResponse = {
data: {
results: [
{
name: {
first: "first",
last: "last",
},
login: {
username: "x",
},
},
],
},
};
export const handlers = [
rest.get('https://randomuser.me/api/', (req, res, ctx) => {
return res(ctx.json({mockResponse}))
}
})
]
VSCODE terminal
Something is going wrong in the return line in handlers array in the handler file. It's not sending back the mockResponse correctly.
Found the mistake. The response structure returned by the actual API and the msw is different. Just had to remove the 'data' object in the mockResponse and keep the it just as an array of 'results'.
import { rest } from 'msw';
const mockResponse = {
results: [
{
name: {
first: "first",
last: "last",
},
login: {
username: "x",
},
},
],
};
export const handlers = [
rest.get('https://randomuser.me/api/', (req, res, ctx) => {
return res(ctx.json(mockResponse))
}
})
]
I am trying to migrate my previously working local state to redux. Now loading available Players works just fine, but deleting will somehow stop in the playerActions.js file, where I dispatch and then return an API Call. So to further give details here are my code parts in relevance:
PlayerPage.js (Component):
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { loadPlayers, deletePlayer } from '../../redux/actions/playerActions';
import PlayerForm from './playerform';
import PlayCard from './playercard';
import PropTypes from 'prop-types';
import { toast } from 'react-toastify';
class PlayerPage extends Component {
constructor(props) {
super(props);
this.handleDeletePlayer = this.handleDeletePlayer.bind(this);
state = {};
componentDidMount() {
const players = this.props;
players.loadPlayers().catch(err => {
alert('Loading players failed. ' + err);
});
}
handleDeletePlayer = player => {
toast.success('Player deleted');
try {
deletePlayer(player);
} catch (err) {
toast.error('Delete failed. ' + err.message, { autoClose: false });
}
};
render() {
const styles = {
margin: '20px'
};
return (
<div className="container-fluid">
<div>
<h2 style={styles}>Add Player</h2>
<div className="container-fluid">
<PlayerForm handleAddNewPlayer={this.handleAddPlayer} />
</div>
</div>
<hr></hr>
<div>
<h2 style={styles}>Available Player</h2>
<div className="container-fluid">
{this.props.players.map(player => (
<PlayCard
player={player}
key={player.id}
imageSource={`${process.env.API_URL}/${player.profileImg}`}
onDeletePlayer={this.handleDeletePlayer}
/>
))}
</div>
</div>
</div>
);
}
}
PlayerPage.propTypes = {
players: PropTypes.array.isRequired
};
function mapStateToProps(state) {
return {
players: state.players
};
}
const mapDispatchToProps = {
loadPlayers,
deletePlayer
};
export default connect(mapStateToProps, mapDispatchToProps)(PlayerPage);
And the Action being called is in here:
playerActions.js:
import * as types from './actionTypes';
import * as playerApi from '../../api/playerApi';
export function loadPlayersSuccess(players) {
return { type: types.LOAD_PLAYERS_SUCCESS, players };
}
export function deletePlayerOptimistic(player) {
return { type: types.DELETE_PLAYER_OPTIMISTIC, player };
}
export function loadPlayers() {
return function(dispatch) {
return playerApi
.getAllPlayers()
.then(players => {
dispatch(loadPlayersSuccess(players));
})
.catch(err => {
throw err;
});
};
}
export function deletePlayer(player) {
console.log('Hitting deletePlayer function in playerActions');
return function(dispatch) {
dispatch(deletePlayerOptimistic(player));
return playerApi.deletePlayer(player);
};
}
The console.log is the last thing the app is hitting. But the API Call is never made though.
API Call would be:
playerApi.js:
import { handleResponse, handleError } from './apiUtils';
const axios = require('axios');
export function getAllPlayers() {
return (
axios
.get(`${process.env.API_URL}/player`)
.then(handleResponse)
.catch(handleError)
);
}
export function deletePlayer(id) {
return (
axios
.delete(`${process.env.API_URL}/player/${id}`)
.then(handleResponse)
.catch(handleError)
);
}
I was like spraying out console.log in different places and files and the last one I am hitting is the one in playerActions.js. But after hitting it the part with return function(dispatch) {} will not be executed.
So if someone could point me in a general direction I'd be more than grateful.
It looks like you are calling your action creator deletePlayer but you aren't dispatching it correctly. This is why the console.log is being called but not the method that does the request.
I'd recommend taking a look at the documentation for mapDispatchToProps to fully understand how this works. In your example, you should just need to change the call to deletePlayer in your PlayerPage component to this.props.deletePlayer() to use the action creator after it's been bound to dispatch properly.
this how the mapDispatchToProps should be:
const mapDispatchToProps = dispatch => {
return {
load: () => dispatch(loadPlayers()),
delete: () => dispatch(deletePlayer()),
}
}
then call load players with this.props.load() and delete player with this.props.delete()
So I have a mock (typemoq) http call that I'm passing into my react component (mounted with enzyme):
const mockhttp: TypeMoq.IMock<IHttpRequest> = TypeMoq.Mock.ofType<IHttpRequest>();
mockhttp
.setup(x => x.get('/get-all-goal-questions'))
.returns(() => {
return Promise.resolve(mockResponse.object.data);
});
const wrapper = mount(<Goals history={Object} http={mockhttp.object} />);
expect(wrapper.find('#foo')).to.have.lengthOf(1);
However, the mock "Get" isn't being called until after the expected, how can I get the expect to wait until the mock is called to test?
// Edit here is the code under test
let httpCall = this.props.pageHoc.httpRequest -- the call im mocking
import React, { Component } from 'react';
import { Row, Col } from 'react-bootstrap';
import { Animated } from "react-animated-css";
import { Answer, IPageHOC } from '../../interfaces/pageObjects';
// Fonts
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome'
import { faCheck } from '#fortawesome/free-solid-svg-icons'
// Cookies
import cookie from 'react-cookies';
// Google analytics
import ReactGA from 'react-ga';
type GoalsComponent = {
answers: Answer[],
showError:boolean,
showAnimation:boolean,
question:string,
questionId:number
};
type Props = {
history:any,
pageHoc?: IPageHOC
}
export default class Goals extends Component<Props, GoalsComponent>
{
constructor(props: any) {
super(props);
this.state = {
answers : [],
showError: false,
showAnimation:false,
question: "",
questionId: 0
}
}
componentDidMount(){
// Hide nav
this.props.pageHoc.hideRightNav();
this.loadQuestions();
}
loadQuestions(){
// Setup auth
let auth = this.props.pageHoc.externalAuth;
auth.setToken(cookie.load('Email'), cookie.load('Password')).then((x) => {
let httpCall = this.props.pageHoc.httpRequest;
// Headers
httpCall.setHeaders({
Organization: cookie.load('Organization')
});
httpCall.get(`/thrive/goal/get-all-goal-questions`)
.then((x) => {
this.setState({
answers:x.data.goalQuestions[0].answers,
question: x.data.goalQuestions[0].question,
questionId: x.data.goalQuestions[0].id
});
})
.catch((x) => {
console.log(x, "error");
});
});
}
render() {
return (
<ul className="list-group list-group-goals">
{this.state.answers.map((x:Answer) =>
<li className={("list-group-item ") + (x.selected ? "selected" : "")} key={x.id} onClick={() => this.toggleGoal(x.id)}>
{x.answer}
<FontAwesomeIcon icon={faCheck} className={("goal-tick ") + (x.selected ? "goal-tick-red" : "")} />
</li>
)}
</ul>
);
}
}
hmm if you are trying to test async request you should follow what is written here:
https://jestjs.io/docs/en/tutorial-async
for the short version your test should look something like this:
it('works with async/await', async () => {
expect.assertions(1);
const data = await user.getUserName(4);
expect(data).toEqual('Mark');
});
You can do something like this:
fun test = async () => {
const mockhttp: TypeMoq.IMock<IHttpRequest> = TypeMoq.Mock.ofType<IHttpRequest>();
mockhttp
.setup(x => x.get('/get-all-goal-questions'))
.returns(() => {
return Promise.resolve(mockResponse.object.data);
});
const wrapper = await mount(<Goals history={Object} http={mockhttp.object} />);
expect(wrapper.find('#foo')).to.have.lengthOf(1);
}
This will wait for the promise returned by the mocked get function to resolve and the component to render with the latest data.
I have the following code where inside my React component I'm using a third party component -- FineUploader in my case.
Upon uploading files, it calls its onComplete function. From here, I'm trying to call my action creators to handle post-upload processes but I'm unable to access my props or actions from there because this is all outside of my component.
Up to that point, everything is working. I'm able to call uploader instance from my component and upload the file to my Azure Blob Storage and get the fileName and blobName once the upload is completed.
It's funny that I'm stuck at the easier part!
Here's my component:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import FineUploaderAzure from 'fine-uploader-wrappers/azure'
// Components
import Gallery from './gallery/index';
// Actions
import * as myActions from '../myActions';
// Instantiate FineUploader
const uploader = new FineUploaderAzure({
options: {
cors: {
expected: true,
sendCredentials: false
},
signature: {
endpoint: 'http://localhost:123/getsas'
},
request: {
endpoint: 'https://myaccount.blob.core.windows.net/my-container'
},
callbacks: {
onComplete: function (id, name, responseJSON, xhr) {
const fileName = uploader.methods.getName(id);
const blobName = uploader.methods.getBlobName(id);
// I now need to call my action creator to handle backend stuff
// Or I can call the handleFileUpload function inside my component.
// How do I access either my action creator or handleFileUpload function from here?
}
}
}
})
class FileUploader extends Component {
constructor(props) {
super(props);
this.handleFileUpload = this.handleFileUpload.bind(this);
}
handleFileUpload(fileName, blobName) {
debugger;
}
render() {
return (
<Gallery uploader={uploader} />
)
}
}
function mapStateToProps(state, ownProps) {
return {
}
}
function mapDispatchToProps(dispatch, ownProps) {
return {
actions: bindActionCreators(myActions, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(FileUploader)
I came up with the following approach that works. I'm not sure if this is the best way or there's a more elegant approach. I won't accept my answer as the correct one and let everyone post their comments and votes.
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import FineUploaderAzure from 'fine-uploader-wrappers/azure'
// Components
import Gallery from './gallery/index';
// Actions
import * as myActions from '../myActions';
// Instantiate FineUploader
const uploader = new FineUploaderAzure({
options: {
cors: {
expected: true,
sendCredentials: false
},
signature: {
endpoint: 'http://localhost:123/getsas'
},
request: {
endpoint: 'https://myaccount.blob.core.windows.net/my-container'
}
}
})
class FileUploader extends Component {
constructor(props) {
super(props);
this.handleFileUpload = this.handleFileUpload.bind(this);
}
componentDidMount() {
uploader.on('complete', (id, name, responseJSON, xhr) => {
const originalName = uploader.methods.getName(id);
const blobName = uploader.methods.getBlobName(id);
this.handleFileUpload(originalName, blobName);
}
}
handleFileUpload(fileName, blobName) {
// Received fileName and blobName. We can call our actions creators here.
this.props.actions.someAction(fileName, blobName);
}
render() {
return (
<Gallery uploader={uploader} />
)
}
}
function mapStateToProps(state, ownProps) {
return {
}
}
function mapDispatchToProps(dispatch, ownProps) {
return {
actions: bindActionCreators(myActions, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(FileUploader)
Suppose I have the following Tree:
<LandingPage>
<PageHeader>
<Menu>
<ShoppingCart>
</Menu>
</PageHeader>
<MainPage>
<ShoppingCart>
</MainPage>
</LandingPage>
The component we care about is the ShoppingCart.
Upon mounting it (componentDidMount) ShoppingCart triggers an action, so that the ShoppingCartStore makes a request to a server and returns a list of articles - triggering a rerender of ShoppingCart .
The way it is set up now, there will always be two requests and two rerenders, because both components are in the dom.
One solution would be to have a common root trigger these requests, but that would be the LandingPage - and one would have to pass the data through PageHeader and Menu and MainPage.
Is there a better solution? Is that good enough?
I use an api.store for all data requests. I call the api.store in the entry app.js. Then I use an action that the api.store listens to for the initial data requests.
app.js
'use strict';
import React from 'react';
import ReactDom from 'react-dom';
import AppCtrl from './components/app.ctrl.js';
import Actions from './actions/api.Actions';
import ApiStore from './stores/Api.Store';
window.ReactDom = ReactDom;
Actions.apiInit();
ReactDom.render( <AppCtrl />, document.getElementById('react') );
api.store
import Reflux from 'reflux';
import Actions from '../actions/api.Actions';
import ApiFct from '../utils/sa.api';
let ApiStoreObject = {
newData: {
"React version": "0.14",
"Project": "ReFluxSuperAgent",
"currentDateTime": new Date().toLocaleString()
},
listenables: Actions,
apiInit() { ApiFct.setData(this.newData); },
apiInitDone() { ApiFct.getData(); },
apiSetData(data) { ApiFct.setData(data); }
}
const ApiStore = Reflux.createStore(ApiStoreObject);
export default ApiStore;
In the component that connects to a store the initial state calls the store data so if the data is already there you get it.
import React from 'react';
import BasicStore from '../stores/Basic.Store';
let AppCtrlSty = {
height: '100%',
padding: '0 10px 0 0'
}
const getState = () => {
return {
Data1: BasicStore.getData1(),
Data2: BasicStore.getData2(),
Data3: BasicStore.getData3()
};
};
class AppCtrlRender extends React.Component {
render() {
let data1 = JSON.stringify(this.state.Data1, null, 2);
let data2 = JSON.stringify(this.state.Data2, null, 2);
let data3 = JSON.stringify(this.state.Data3, null, 2);
return (
<div id='AppCtrlSty' style={AppCtrlSty}>
React 0.14 ReFlux with SuperAgent<br/><br/>
Data1: {data1}<br/><br/>
Data2: {data2}<br/><br/>
Data3: {data3}<br/><br/>
</div>
);
}
}
export default class AppCtrl extends AppCtrlRender {
constructor() {
super();
this.state = getState();
}
componentDidMount = () => { this.unsubscribe = BasicStore.listen(this.storeDidChange); };
componentWillUnmount = () => { this.unsubscribe(); };
storeDidChange = (id) => {
switch (id) {
case 'data1': this.setState({Data1: BasicStore.getData1()}); break;
case 'data2': this.setState({Data2: BasicStore.getData2()}); break;
case 'data3': this.setState({Data3: BasicStore.getData3()}); break;
default: this.setState(getState());
}
};
}
From https://github.com/calitek/ReactPatterns React.14/ReFluxSuperAgent.