I have always been fascinated with one’s ability to create or recreate. Software development for gives me a way to translate my imaginations into useful tangible things. The feeling I get after going through challenges to figure out how to put my ideas together, is incomparable. Full-stack development is the combination of both front-end technologies and back-end technologies to create a fully functional application. In this tutorial, we are going to combine front-end technology called React and Redux with a back-end technology called Rails to create a simple sign-up system.
Note: This is an advanced topic, basic to intermediate knowledge in React Redux and Ruby on rails is a prerequisite for this tutorial. Visit this link for React tutorial, for Redux tutorial and rails tutorial.
In summary, React is a declarative, efficient, and flexible JavaScript library for building user interfaces. It lets you compose complex UIs from small and isolated pieces of code called “components”. Each component may have data specific to it called state or data from other components as props or it may not have data at all. Redux is a predictable state container for JavaScript apps. On the other hand, Rails is just a back-end technology for persisting data.
Let us build a simple signup system with the three technologies mentioned above.
In this tutorial, we will create a react API that communicates with rails API to perform sign up. Our Sign up UI will be a form with the following fields (username, email, password, and image).
Steps for setting up Front-end
Let’s start by creating a sign-up page.
Create action creators and reducers to update the redux state
Send user data to the back-end for persistence with Axios.
Steps for setting up Back-end
Create your rails API
Create a DB for API
Create a user table and model
Migrate table
Create a users_controller to handle data persistence
Create react app
make sure you have node install and create a react project with the command below
npx create-react-app signup
This command will generate your basic react-app that you can start building your application on.
Create the UI for sign up
create a file to house your sign-up component .ie ./src/SignUp.js
create a react class component inside the
SignUp.js
The sign-up component at this moment is going to be a basic react application that renders a form and has event handlers to track the changes in the state of each input field.
import React, { Component } from 'react';
class Signup extends Component {
constructor(props) {
super(props);
this.state = {
username: '',
email: '',
password: '',
picture: '',
};
}
handleUsernameChange = (e) => {
this.setState({
username: e.target.value,
});
}
handleEmailChange = (e) => {
this.setState({
email: e.target.value,
});
}
handlePasswordChange = (e) => {
this.setState({
password: e.target.value,
});
}
handleAvatarChange = (e) => {
this.setState({
picture: e.target.files[0],
});
}
render() {
return (
<form onSubmit={this.onSubmitHandler}>
<h1>{message}</h1>
<input
onChange={this.handleUsernameChange}
type="text"
name="username"
placeholder="Username"
required
/>
<input
onChange={this.handleEmailChange}
type="email"
name="email"
placeholder="Email"
required
/>
<input
onChange={this.handlePasswordChange}
type="password"
name="password"
placeholder="Password"
required
/>
<input
onChange={this.handleAvatarChange}
type="file"
accept="/images/*"
/>
<button type="submit">
Create Account
</button>
</form>
);
}
}
export default Signup;
Connect sign-up component to Redux
Bind React to Redux by issuing the command below, then import and use themethod fromconnect
library to bind React component(sign-up component) to Reduxreact-redux
npm i react-redux
import
connect
inside the SignUp.js.
import { connect } from 'react-redux';
with this set up we can now mapStateToProps or mapDispatchToProps, after your Redux store is set up.
At this point we have to set up Redux action and Redux reducers to be able to either mapStateToProps or mapDispatchToProps. In other words, read from the Redux store or send data to the Redux store. Inside the the Redux action we will dispatch a function to our reducer as we also persist data to our back-end Rails Api with axios. let us install axios for enable us use axios.
npm i axios
read more on axios from the axios documentation
Let create these files inside
./src/redux/action.js, ./src/redux/reducer.js,
now we implement the Redux actions asimport axios from 'axios';
const CREATE_USER = 'CREATE_USER';
const CREATE_USER_ERROR = 'CREATE_USER_ERROR';
const createUser = newUser => async (dispatch) => {
try {
dispatch({ type: CREATE_USER, ...newUser });
const response = await axios({
method: 'POST',
url: 'Api route for posting data to database',
data: { user: newUser },
crossdomain: true,
});
const { token } = response.data;
localStorage.setItem('jwt', token);
} catch {
dispatch({ type: CREATE_USER_ERROR });
}
};
export { createUser }
The createUser method is going to send data to our rails API and wait for a response. I also sends data to update the Redux store.
We are going to configure our API to return some
Json
data that would contain information about whether our request from the front end to sign-up a user was successful or not. If successful, we will login the user right after successful signup and maintain a session for the user. To maintain a session for user, we store the token generated from the back-end response(returned as part of our positive response) inside localStorage. This is handled in these lines
const { token } = response.data;
localStorage.setItem('jwt', token);
we are storing tho token in the localstorage hash on the key 'jwt'
Now we have to create our reducer for the
CREATE_USER and CREATE_USER_ERROR
const initialState = {
isLogin: false,
user: {
username: '',
email: '',
password: '',
picture: '',
}
};
const authReducer = (state = initialState, action) => {
switch (action.type) {
case 'CREATE_USER': return {
...state.user,
isLogin: true,
user: {
username: action.username,
email: action.email,
password: action.password,
picture: action.picture,
},
};
case 'CREATE_USER_ERROR': return {
isLogin: false,
};
default: return state;
}
};
export default authReducer;
CREATE_USER_ERROR is dispatched when a negative response is returned from our server. With this in place we almost good to go. But there would be two problems if we try to signup a user. we need to properly handle the picture attribute and set up redux-thunk middleware since we are dispatching a function in our createUser action creator.
To handle our image upload, we will store our image to cloudinary and copy the path of the image in cloudinary to our database.
storing images directly into the server is not recommended. you it is good to use third parties such amazon s3, cloudinary, etc...
for this we will update our S
ignUp.js
to include handleImageUpload() and onSubmitHandler().
handleImageUpload()
will be responsible for storing the image file selected to cloudinary and return the URL reference of that image. This URL reference is then sent as part of data dispatch to redux and the rails back-end.onSubmitHandler(),
this method on the other hand will be responsible for sending data to our action creator. We shall import the createUser
action creator method, and passed data to from the form to createUser({sign up state data}) to be update the redux store and send data to the rails API. In this app createUser
will mapped to newUser
, hence we will reference create user as NewUser
. mapDispatchToProps= dispatch=>({
createUser: newUser
})
we destructure state data and get the URL reference, pass it as an argument to newUser, for dispatch.
const {username, email, password,picture}=this.state
//get image path, from handleImageUpload
newUser({username,email,password,picture})
Check below for full implementation.
handleImageUpload = async (imageFile) => {
const formData = new FormData();
formData.append('file', imageFile);
formData.append('upload_preset', 'avatar');
const response = await axios({
url: 'your cloudinary api url',
method: 'POST',
data: formData,
});
return response.data.secure_url;
}
onSubmitHandler = async (e) => {
e.preventDefault();
const {username, email, password } = this.state;
let { picture } = this.state;
const { newUser, user } = this.props;
const imgPath = await this.handleImageUpload(picture);
picture = imgPath;
await newUser({username, email, password, picture});
if (user.isLogin === true) {
const { history } = this.props;
history.push('/');
} else {
this.setState(
{
message: 'welcome',
},
);
}
}
with this in place we can mapDispatchToProps() and mapStateToProps() to dispatch our data to redux, and read data from the store. Check below.
const mapStateToProps = state => ({
user: state.user,
});
const mapDispatchToProps = dispatch => ({
newUser: estate => dispatch(createUser(estate)),
});
Signup.propTypes = {
newUser: PropTypes.func.isRequired,
user: PropTypes.object.isRequired,
history: PropTypes.func.isRequired
};
export default connect(mapStateToProps, mapDispatchToProps)(Signup);
our resultant SignUp.js code will become like this
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import axios from 'axios';
import { createUser } from '../../redux/actions/authAction';
class Signup extends Component {
constructor(props) {
super(props);
this.state = {
username: '',
email: '',
password: '',
picture: '',
message: '',
};
}
handleUsernameChange = (e) => {
this.setState({
username: e.target.value,
});
}
handleEmailChange = (e) => {
this.setState({
email: e.target.value,
});
}
handlePasswordChange = (e) => {
this.setState({
password: e.target.value,
});
}
handleAvatarChange = async (e) => {
this.setState({
picture: e.target.files[0],
});
}
handleImageUpload = async (imageFile) => {
const formData = new FormData();
formData.append('file', imageFile);
formData.append('upload_preset', 'avatar');
delete axios.defaults.headers.common.Authorization;
const response = await axios({
url: 'your cloudinary api url',
method: 'POST',
data: formData,
});
return response.data.secure_url;
}
onSubmitHandler = async (e) => {
e.preventDefault();
const {
username, email, password,
} = this.state;
let { picture } = this.state;
const { newUser, user } = this.props;
const imgPath = await this.handleImageUpload(picture);
picture = imgPath;
await newUser({
username, email, password, picture,
});
if (user.isLogin === true) {
const { history } = this.props;
history.push('/');
} else {
this.setState(
{
message: 'welcome',
},
);
}
}
render() {
const { message } = this.state;
return (
<form onSubmit={this.onSubmitHandler} >
<h1>{message}</h1>
<input onChange={this.handleUsernameChange} type="text" required />
<input onChange={this.handleEmailChange} type="email" required />
<input onChange={this.handlePasswordChange} type="password" required />
<input onChange={this.handleAvatarChange} type="file" accept="/images/*" />
<Link to="/login">login</Link>
<button type="submit">Create Account</button>
</form>
);
}
}
const mapStateToProps = state => ({
user: state.user,
});
const mapDispatchToProps = dispatch => ({
newUser: estate => dispatch(createUser(estate)),
});
Signup.propTypes = {
newUser: PropTypes.func.isRequired,
user: PropTypes.object.isRequired,
history: PropTypes.object.isRequired,
};
export default connect(mapStateToProps, mapDispatchToProps)(Signup);
let us create a store and add redux-thunk middleware to allows us to write action creators that return a function instead of an action. move to index.js and set it up like this.
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 rootReducer from './redux/reducers/index';
import App from './component/App';
const initialState = {
user: {
isLogin: false,
newuser: {
username: '',
email: '',
password: '',
picture: '',
},
},
};
const store = createStore(rootReducer, initialState, applyMiddleware(thunk));
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root'),
);
now our front-end is ready, we can now focus on the back-end
Create rails Api
rails new backend --api --database=postgresql -T
rails g model User username:string email:string password:string picture:string
rails db:migrate
Now we include
jwt gem
and rack-cors gem
in gemfile.then we run
bundle install
in console.configure cors to accept request from a source of your choice. Cors setting determines which domain should your api grant access to. read more on rack-cor gem we will configure our cors to accept request from all domain source by setting
origin '*' in config/application.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins '*'
resource '*',
headers: :any,
methods: %i[get post put patch delete options head]
end
end
we are going to skip testing and also ignore validations. We are going to focus on the controller and its actions. Let create a controller for our user model
rails g controller users
we will new a create action to create a user, inside our users_controller.rb we create a create method with this code.
class UsersController < ApplicationController
def create
new_user = User.new(user_params)
if new_user.save
token = JsonWebToken.encode(user_id: new_user.id)
time = Time.now + 24.hours.to_i
render json: { token: token, time: time }, status: :ok
else
head(:unprocessable_entity)
end
end
private
def user_params
params.require(:user).permit(
:username,
:email,
:country,
:password,
:picture
)
end
end
create user method accept parameters and save it to database. Then get the user's id, encrypt the id with
jwt gem
and add a time stamp to the encrypted user_id. The time stamp determines the time of expiry of the token, in this case, it expires after 24hrs. This means a user's session will be destroyed after 24hrs from sign up.we can now set up our route inside
config/route.rb
resources :users, only: %i[create]
Get front end to communicate with back-end
run rails routes in console to view the url for your api. In this case our post requests is going to be made on
backend_domain_name/users
Conclusion
Now we will be able to save a users information inside our rails back-end with these steps and returned a token to maintain a session. A similar result can be used for sign in just that for signing in the instead of saving we compare email and password to check if they exist then we create a token that would be used by user maintain a session.
Note this token are sent as part of the response to our the front end. The session handling is then than in the front end.